话说,这个NDK,其实学习了许多遍,之前项目上也没有什么地方用,就导致了一个问题,那便是学了忘记,忘记了学,我总结了下,那便是没有写笔记,没有写笔记,就没有所谓的回头看笔记的概念了。
可是呢,现已2024年了,是吧,鸿蒙的呈现,就导致了这个C和C++ 不得不学了,可是仍是有倾向性的,我的意图是学完后,看得懂github 上的一些简略代码就行,毕竟现在的我也不大可能进得了全手写C和C++的项目,所以核心仍是在UI业务层,可是这个玩意不可不会,鸿蒙出来了,Android 不可能就死了,感觉就业机会的变多了(这个玩意是相对的,假如自身需求都到不到我头上,反而是减少了就业机会),当然对我这种技能进退两难的人而言,已然某个范畴精不了,那么就得多会,现在恰恰是学习这个玩意的好时机,鸿蒙出来了,许多C或C++ 工程在Android上可用的,那么就会被搬到上面去,咱们的意图便是等大佬搬完了,起码看得懂一些,不至于像现在Android,大佬各种规划形式一包,看的打脑壳,现在还在初中期,有许多大佬开端整blog,那么就能够趁机学习一波。至于说为什么不看之前的Android blog,许多demo运转不起来也是一个原因,编译形式也不一样,许多细节,不经历就完全搞不懂。
已然清晰了方针,咱们核心仍是稳固Android,然后学习NDK,那么最好的办法,除了一个教师手把手教学,那便是自己看Google 供给的Demo了,最近看鸿蒙的Demo 发现的,本来Android 官方也供给了许多Demo,哭死,英语也得补。
OK,那就开整,咱们从第一个Demo,测验去逐行理解。
资料
正文
这个工程很简略,便是经过调用C或许C++供给的函数回来一个字符串。
装备
build.gradle
装备NDK的版别:
ndkVersion '25.1.8937393'
许多老的ndk 项目跑不起来,往往加上这个能够跑起来,可是得注意版别。
装备cmake:
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
这个阐明是经过cmake 编译静态库和动态库的。假如说这种编译形式,这个文件是必须的。
cpp
cmakeLists.txt 文件。
cmake_minimum_required(VERSION 3.18.1)
project("hello-jni")
add_library(hello-jni SHARED
hello-jni.cpp)
target_link_libraries(hello-jni
android
log)
- cmake_minimum_required 的版别
- project 项目名称。
- add_library 导入的c或c++的lib,hello-jni 表明这个是库的名称,SHARED 指定库的类型,表明是同享库,hello-jni.cpp 表明这个是lib 的源文件。
- target_link_libraries 表明:这行代码的意思是:“将
android
和log
这两个库链接到hello-jni
方针。” 这两个库,反正是需求的,我注释掉会报错。
源码
hello-jni.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from JNI.";
return env->NewStringUTF(hello.c_str());
}
- include 导入库,jni和string
- extern “C” JNIEXPORT jstring JNICALL ,JNI是C代码,可是是C++工程,所以需求添加 extern “C”,假如不添加这个调调会呈现函数找不到。JINEXPORT 和JNICALL 是JNI的宏,用于界说本地办法如何从Java调用,JNIEXPORT是用来界说导出函数的关键字,JNICALL则用于指定函数调用约好。jstring 表明这个函数回来了一个Java 的字符串目标。
- std::string hello = “Hello from JNI.”; 界说了一个c++的 string 目标。
- env->NewStringUTF(hello.c_str()) 创立一个Java 的字符串目标然后回来。 env->NewStringUTF(“Hello from JNI. 直接赋值”) 这么也能够。
- jobject仍是jclass是合JNI函数类型有关,假如说静态函数则是jclass。
从上面的代码 咱们能够看出,jstring 用于界说函数的回来值,先界说了一个C++的字符串,然后经过env 创立了一个Java 目标。JNIEnv 能够对Java 目标进行如下操作:创立Java 目标,调用Java 目标办法,获取Java目标特点,所以NewStringUTF是创立了一个Java 的字符串目标。
kotlin 层的代码
class HelloJni : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityHelloJniBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.helloTextview.text = stringFromJNI()
}
external fun stringFromJNI(): String?
companion object {
init {
System.loadLibrary("hello-jni")
}
}
}
- System.loadLibrary(“hello-jni”) 导入咱们创立的hello-jni 的lib,这种属于静态导入。
- external fun stringFromJNI(): String? 这种是kotlin 中界说的external 界说native 函数。这个地方能够看出jobject 其实便是HelloJni 这个目标。
扩展
env 常用函数
- NewObject 创立Java 类目标
- NewString 创立字符串目标
- NewArray 创立type的数组目标
- GetField 获取 字段
- SetField 设置 字段
- get/setStaticField 获取或设置静态字段不能
- callMethod 调用回来值为type的办法。
- callStaticMethod 调用回来值为type 的静态办法
根据NewObject创立Int
extern "C" JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_intFromJNI(JNIEnv *env, jobject thiz) {
// 获取到class
jclass intClass = env->FindClass("java/lang/Integer");
// 结构函数
jmethodID constructor = env->GetMethodID(intClass, "<init>", "(I)V");
jobject intObj = env->NewObject(intClass, constructor, 5);
return intObj;
}
经过这个例子,咱们能够把握几个知识点:
- 首要需求class 的包明
- 默许的结构函数,例如int 的是:(I)V,I表明入参类型,V表明没有回来值。假如括号里边填了值,那么这个玩意需求写默许值。
根据NewObject创立User
User的class:
class User(){
var name:String="默许的用户"
var age:Int=5
}
函数:
external fun userFromJNI(): User?
JNI函数:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_userFromJNI(JNIEnv *env, jobject thiz) {
// 创立Java 目标
jclass userClass = env->FindClass("com/example/hellojni/User");
jmethodID constructor = env->GetMethodID(userClass, "<init>", "()V");
jobject intObj = env->NewObject(userClass, constructor);
// 直接调用特点 设置name
jfieldID nameFieldId = env->GetFieldID(userClass, "name", "Ljava/lang/String;");
env->SetObjectField(intObj, nameFieldId, env->NewStringUTF("Hello from JNI. 直接赋值"));
// 调用set 设置年龄
jmethodID ageMid = env->GetMethodID(userClass, "setAge", "(I)V");
env->CallVoidMethod(intObj,ageMid,55);
return intObj;
}
根据NewObject创立Student
class,这个主要是多个入参的结构函数:
public class Student {
public String name;
public int age;
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
native:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_studentFromJNI(JNIEnv *env, jobject thiz) {
jclass userClass = env->FindClass("com/example/hellojni/Student");
jmethodID constructor = env->GetMethodID(userClass, "<init>", "(Ljava/lang/String;I)V");
jobject intObj = env->NewObject(userClass, constructor,env->NewStringUTF("张3"),5);
return intObj;
}
改造一下,承受外部传入参数:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_studentFromJNI(JNIEnv *env, jobject thiz, jstring name,
jint age) {
jclass userClass = env->FindClass("com/example/hellojni/Student");
jmethodID constructor = env->GetMethodID(userClass, "<init>", "(Ljava/lang/String;I)V");
jobject intObj = env->NewObject(userClass, constructor,name,age);
return intObj;
}
我记得这个地方有问题,便是这个age 的类型是jint,可是咱们NewObject 传入的不能是jint,所以会报错,抽时间补一下。
直接调用jobject 中的函数
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_HelloJni_setNativeCall(JNIEnv *env, jobject thiz) {
jclass callClass = env->FindClass("com/example/hellojni/HelloJni");
jmethodID nativeCall = env->GetMethodID(callClass, "nativeCall", "()V");
env->CallVoidMethod(thiz,nativeCall);
}
总结
经过这次的demo,我能够看到Java 目标和C和C++的目标是不能直接互通的,需求经过JNI 转化一层。Java 传入进入到需求经过JNIEnv 转化一次,传入的现已自己转化了。所以习惯反射的调用仍是很重要的。无论是办法仍是特点,都需求先获取到class,然后获取到办法特点,最后才是调用,静态函数则不需求创立目标,昨天请教了一个C大佬,大佬提点我说,通讯应该走socket,很好的规划,很好的高度,直接调用确实是实现上简略了,便是写socket双端的代码量就多了,感觉仍是得看情况吧。
当然了。这里边还有一些问题没有解决回答,比如说,咱们学习C的时候,有一个概念,那便是C没有JAVA这种GC机制,可是咱们上面写的一趴啦代码,却没有任何一句代码是用于GC的,可是这又是一个C++的代码,有点打脑壳,后续再整。