一、Native 方法的注册
1.1 静态注册
可以参照系统中的 MediaRecorder.java 写一个简单的静态注册的范例:
1 | public class MediaRecorder { |
在 static 代码块中我们调用 System.loadLibrary(“media_jni”); 来加载 so 的。如果让我们手动来编译 MediaRecorder.java 生成 JNI 方法,那么可以这样:
javac com/example/MediaRecorder.java
javah com .example .MediaRecorder
其中第一个命令会将 java 编译成 .class 文件,第二个 javah 命令会在当前目录(media/src/main/java)中生成 com_example _MediaRecorder.h 这种头文件 !这个头文件会包含如下代码:
1 | JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init |
方法 Java_com_example_MediaRecorder_native_1init 以 Java_开头说明是在 Java 平台中调用 JNI 方法的,后面的 com_example_MediaRecorder_native_1init 指的是 包名 + 类名 + 方法名 的格式。其中参数JNIEnv是 Native 世界中 Java 环境的,通过 JNIEnv 指针就能在Native 访问 Java 世界的代码,不过,它只在创建它的线程中有效,不能跨线程传递。里面的 jclass 对应 Java 中的 java.lang.Class 实例,很多 java 中的类型对应到 Native 就是在前面加一个 j ,尤其是基本类型(基本类型除了 void ,其他的都是)。
1.1.1 静态注册后的调用
当我们在 Java 中调用 native_init 这个 native 方法时,就会从 JNI 中寻找 Java_com_example_MediaRecorder_native_1init 函数,如果没有就会报错,如果找到,就会建立 <native_init ,Java_com_example_MediaRecorder_native_1init> 映射,其实就是保存 JNI 函数指针,这样下次再调用就直接使用这个函数指针,避免每次重复查找。
静态注册就是根据方法名,将 Java 方法和 JNI 函数建立关联!这种静态注册的方式有一些缺点:
JNI 层的函数名称过长
声明 Native 方法的类需要用 javah 生成头文件
初次调用 Native 方法时需要建立关联,影响效率
如果 Java 的 Native 方法知道它在 JNI 中对应的函数指针,就能避免上述缺点,这就是动态注册。
1.2 动态注册
系统的 MediaRecorder 采用的就是动态注册。JNI 中有一个用来记录 Java 方法和 Native 方法映射的数据结构: JNINativeMethod ,代码如下:
1 | typedef stuct { |
有了数据结构我们就能映射了,在 MediaRecorder 的 Native 代码中是这样的:
1 | static const JNINativeMethod gMethods[] = { |
忽略了其他方法,说明一下:
第一个参数 “start” 就是 Java 方法的名字,
第二个参数 “()V” 是签名,为什么需要签名呢?因为 Java 中方法是可以重名的,只是入参或者返回值不同,有这个签名可以区分不同的方法;
第三个参数是 jni 中方法名
在System.loadLibrary 调用加载这个 so 之后,就会执行 register 来注册这种映射关系。体现在 Android 源码上就是在 AndroidRuntime。registerNativeMethods 函数的时候,注册系统需要的这些 native 方法。
1.3 方法签名
方法签名格式就是 :(参数签名格式… )返回值签名格式,比如有些签名是这样的:
1 | "(Ljava/lang/Object; Ljava/lang/String;Lava/lang/String;)V" |
这就说明这个方法入参是 Object,String,String ;返回值是 void
二、数据类型的转换
2.1 基本数据类型的转换
基本类型转换关系如下所示:
2.2 引用数据类型的转换
三、解析 JNIEnv
它是 Java 环境的代表,它只能在创建它的线程有效,不能跨线程传递。,它的主要作用有 2 点:
调用 Java 方法
操作 Java (中的变量和对象等)
在 jni.h 文件中除了定义 JNIEnv 还定义了 JavaVM ,JavaVM 代表的是 Java 虚拟机,每一个进程中只有一个 JavaVM,因此,该进程所有的线程都能使用这个 JavaVM ,通过 JavaVM 的 AttachCurrentThread 就能获取这个线程的 JNIEnv ,这样就能在不同的线程中调用 Java 方法了。
注意:使用了 AttachCurrentThread 的情况下,在线程退出前务必调用 DetachCurrentThread 函数来释放资源 !
我们知道, JNIEnv 是 JNINativeInterface* 类型的,它里面包含了很多函数,这里列举 3 个比较常用的函数:
FindClass : 用来找到 Java 中指定名称的类
GetMethodID:用于获取 Java 中的方法
GetFieldID:用来得到 Java 中的成员变量
平时使用一般是这样的:
1 | static void |
上述保存clazz 和 jfildID 以及 jmethodID 类型的 变量有2个原因:
为了效率考虑,如果每次调用相关方法时都要查询方法和变量,效率会变低
成员变量是本地引用,在 android_media_MediaRecorder_native_ini函数返回时就会自动释放
四、引用类型
略