0%

第9章:JNI原理

一、Native 方法的注册

1.1 静态注册

可以参照系统中的 MediaRecorder.java 写一个简单的静态注册的范例:

1
2
3
4
5
6
7
8
public class MediaRecorder {
static {
System.loadLibrary("media_jni");
native_init();
}

private static native final void native_init();
}

在 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
2
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init 
(JNIEnv *, jclass) ;//1

方法 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
2
3
4
5
typedef stuct {
const char* name;//Java 方法的名字
const char* signature;//方法签名
void* fnPtr;//JNI中对应的方法指针
}

有了数据结构我们就能映射了,在 MediaRecorder 的 Native 代码中是这样的:

1
2
3
static const JNINativeMethod gMethods[] = {
{"start", "()V", (void *)android_media_MediaRecorder_start}
}

忽略了其他方法,说明一下:

  • 第一个参数 “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 基本数据类型的转换

基本类型转换关系如下所示:

JNI基本类型转换关系

2.2 引用数据类型的转换

JNI引用类型映射

三、解析 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void
android_media_MediaRecorder_native_init(JNIEnv *env) {
jclass clazz;

clazz = env -> FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}

fields.context = env -> GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}

fields.surface = env -> GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
if (fields.surface == NULL) {
return;
}

jclass surface = env -> FindClass("android/view/Surface");
if (surface == NULL) {
return;
}

fields.post_event = env -> GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");
if (fields.post_event == NULL) {
return;
}
}

上述保存clazz 和 jfildID 以及 jmethodID 类型的 变量有2个原因:

  • 为了效率考虑,如果每次调用相关方法时都要查询方法和变量,效率会变低

  • 成员变量是本地引用,在 android_media_MediaRecorder_native_ini函数返回时就会自动释放

四、引用类型

谢谢你的鼓励