Android JNI 原理

JNI 是 Java Native Interface 的缩写,译为 Java 本地接口,是 Java 与其他语言通信的桥梁(Android 中更多的是为了方便 Java 调用 C,C++ 等本地代码所封装的一层接口),可用它来解决一些 Java 无法处理的任务(Java 的跨平台特性导致其本地交互能力不够强大,一些和操作系统相关的特性 Java 无法完成),一般在以下情况需要用到 JNI 技术:

  • 需要调用 Java 语言不支持的依赖于操作系统平台特性的一些功能。例如:需要调用当前的 UNIX 系统的某个功能。

  • 为了整合一些以前的非 Java 语言开发的系统。例如:需要用到早期实现的 C/C++ 语言开发的一些功能或系统,将这些功能整合到当前的系统或新的版本中。

  • 为了节省程序的运行时间,必须采用其他语言(比如C/C++)来提升运行效率。例如:游戏、音视频开发涉及的音视频编解码和图像绘制需要更快的处理速度。

为了更方便地使用 JNI 技术,Android 还提供了 NDK 这个工具集合,NDK 开发是基于 JNI 的,通过 NDK 可以 Android 中更加方便地通过 JNI 来访问本地代码,比如 C 或 C++,它和 JNI 开发本质上并没有区别,NDK 还提供了交叉编译器,开发人员只需要简单地修改 mk 文件就可以生成特定 CPU 平台的动态库。使用 NDK 的好处:

  • 提高代码安全性。因为 so 库反编译比较困难。
  • 可方便使用已有的 C/C++ 开源库。
  • 便于平台间的移植。通过 C/C++ 实现的动态库可方便地在其他平台使用。
  • 提高程序在某些特定情形下的执行效率,但是并不能明显提升 Android 程序的性能。

JNI 的开发流程

JNI 调用 Java 方法的流程是,先通过类名找到类,然后根据方法名找到方法,最后就可以调用这个方法了。
JNI 的开发流程:

1、在 Java 中声明 native 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.testapplication;

public class JNITest {

static {
// 加载动态库,jni-test 是 so 库的标识,so 库完整的名称为 libjni-test.so,这是加载 so 库的规范。
System.loadLibrary("jni-test");
}

public static void main(String args[]){
JNITest jniTest = new JNITest();
System.out.println(jniTest.get());
jniTest.set("hello world");
}

/**
* 声明的 native 方法,需要在 JNI 中实现的。
* @return
*/
public native String get();
public native void set(String str);
}

2、编译 Java 源文件得到 class 文件,然后通过 javah 命令导出 JNI 的头文件(.h 文件)

cd 到 testapplication 目录下,执行命令:javac JNITest.java -h . (注意后面的那个点),会在同级目录下生成 .class 文件和 .h 文件。(关于命令,我是 Mac 系统,并且我的 jdk 版本取消了 javah 命令,所以用上面的命令代替)

3、实现 JNI 方法

JNI 方法指的是 Java 中声明的 native 方法。接下来分别使用 C 和 C++ 来实现,它们的实现过程是类似的,只有少量区别。

需要三个文件:com_example_testapplication_JNITest.h 是刚才生成的 .h 文件。。通过 test.c 和 test.cpp 文件来分别实现 C 和 C++。

com_example_testapplication_JNITest.h
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
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testapplication_JNITest */

#ifndef _Included_com_example_testapplication_JNITest
#define _Included_com_example_testapplication_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_testapplication_JNITest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_testapplication_JNITest_get
(JNIEnv *, jobject);

/*
* Class: com_example_testapplication_JNITest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_example_testapplication_JNITest_set
(JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// test.c
#include "com_example_testapplication_JNITest.h"
#include <stdio.h>


JNIEXPORT jstring JNICALL Java_com_example_testapplication_JNITest_get(JNIEnv *env, jobject thiz){
printf("invoke get in c\n");
return (*env)->NewStringUTF(env,"Hello from JNI !");
};


JNIEXPORT void JNICALL Java_com_example_testapplication_JNITest_set(JNIEnv *env, jobject thiz, jstring string){
printf("invoke get in c\n");
char* str = (char*)(*env)->GetStringUTFChars(env,string,NULL);
printf("%s\n",str);
(*env)->ReleaseStringUTFChars(env,string,str);
};
test.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// test.cpp
#include "com_example_testapplication_JNITest.h"
#include <stdio.h>


JNIEXPORT jstring JNICALL Java_com_example_testapplication_JNITest_get(JNIEnv *env, jobject thiz){
printf("invoke get in c++\n");
return env->NewStringUTF("Hello from JNI !")
};


JNIEXPORT void JNICALL Java_com_example_testapplication_JNITest_set(JNIEnv *env, jobject thiz, jstring string){
printf("invoke get in c++\n");
char* str = (char*)env->GetStringUTFChars(string,NULL);
printf("%s\n",str);
env->ReleaseStringUTFChars(string,str)
};

4、编译 so 库并在 Java 中调用

将 C/C++ 代码编译成本地动态库文件,动态库文件名命名规则:lib+动态库文件名+后缀(操作系统不一样,后缀名也不一样)如:

  • Mac OS X : libHelloWorld.jnilib
  • Windows :HelloWorld.dll(不需要 lib 前缀)
  • Linux/Unix:libHelloWorld.so
1
gcc -dynamiclib -o /Users/jianghouren/libHelloWorld.jnilib jni/test.c -framework JavaVM -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin  

$JAVA_HOME目录在:Mac下查看已安装的 jdk 版本及其安装目录,打开终端,输入:/usr/libexec/java_home -V

参数选项说明:

  • -dynamiclib:表示编译成动态链接库
  • -o:指定动态链接库编译后生成的路径及文件名
  • -framework JavaVM -I:编译 JNI 需要用到 JVM 的头文件(jni.h),第一个目录是平台无关的,第二个目录是与操作系统平台相关的头文件

调用这里出了问题,暂时记录下,一直提示找不到类 JNITest。

NDK 的开发流程

NDK 的开发流程:

1、下载并配置 NDK

2、创建一个 Android 项目,并声明所需的 native 方法

3、实现 Android 项目中所有声明的 native 方法

4、切换到 jni 目录的父目录,然后通过 ndk-build 命令编译产生 so 库

系统源码中的 JNI

Android 系统按语言可划分为 Java 世界和 Native 世界,而 JNI 就是连接它们的桥梁。通过 JNI,它们就可以互相访问对方的代码了。

MediaRecorder 框架中的 JNI

MediaRecorder 用于录音和录像。MediaRecorder 框架中的 JNI 分为:

  • Java Framework 层。对应的是 MediaRecorder.java ,也就是在应用开发中直接调用的类。
  • JNI 层。对应的是 libmedia_jni.so,它是一个 JNI 的动态库。
  • Native 层。对应的是 libmedia.so,这个动态库完成了实际的调用功能。

各层级的 MediaRecorder 源码理解

Java Framework 层

只需要加载对应的 JNI 库,并将相关方法声明为 native(表示它是一个 native 方法,由 JNI 来实现),剩下的工作由 JNI 层来完成。

JNI 层

在 Java Framework 层声明为 native 的方法在 JNI 层都会有对应的方法实现,而这种对应关系是通过 JNI 方法注册实现的,Java Native 方法注册分为静态注册和动态注册,前者多用于 NDK 开发,后者多用于 Framework 开发。

Java Native 方法注册

静态注册

示例(Java):仿照系统,新建一个 MediaRecorder.java 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.testapplication;

public class MediaRecorder {

static {
// 加载名为 media_jni 的动态库,也就是 libmedia_jni.so。
System.loadLibrary("media_jni");
// 内部会调用 Native 方法,用来完成 JNI 的注册。
native_init();
}

// 将方法声明为 native
private static native final void native_init();
public native void start() throws IllegalAccessException;
}

cd 到 testapplication 目录下,执行命令:javac MediaRecorder.java -h .,会在同级目录下生成 .class 文件和 .h 文件。(关于命令,我是 Mac 系统,并且我的 jdk 版本取消了 javah 命令,所以用上面的命令代替)

com_example_testapplication_MediaRecorder.h
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testapplication_MediaRecorder */

#ifndef _Included_com_example_testapplication_MediaRecorder
#define _Included_com_example_testapplication_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_testapplication_MediaRecorder
* Method: native_init
* Signature: ()V
*
对应了上面的 native_init(),以 Java 开头说明是在 Java 平台中调用的 JNI 方法,后面是以包名+类名+方法名的格式。
--------------------------------------------------------------------------------------------
原本在 Java 中应该是以 “.” 进行分割,这里使用了 “ ”(空格),这是因为在 Native 语言中 “.” 有特殊的含 义。
--------------------------------------------------------------------------------------------
native_1init 中多了 “1” 是因为 Java 的 native_init 方法中包含了 “_”,转换成 JNI 方法后会变成 “_1”。
--------------------------------------------------------------------------------------------
JNIEnv 是 Native 世界中 Java 环境的代表,表示一个指向 JNI 环境的指针,通过 JNIEnv *指针就可以在 Native 世界中访问 Java 世界的代码进行操作(通过它来访问 JNI 提供的接口方法),它只在创建它的线程中有效,不能跨线程传递。
--------------------------------------------------------------------------------------------
jclass 是 JNI 的数据类型,对应 Java 的 java.lang.Class 实例。
*/
JNIEXPORT void JNICALL Java_com_example_testapplication_MediaRecorder_native_1init
(JNIEnv *, jclass);

/*
* Class: com_example_testapplication_MediaRecorder
* Method: start
* Signature: ()V
*
* 对应了上面的 start()。
* jobject 也是 JNI 的数据类型,对应于 Java 的 Object。表示 Java 对象中的 this。
--------------------------------------------------------------------------------------------
* JNIEXPORT 和 JNICALL ,它们是 JNI 中所定义的宏,可以在 jni.h 这个头文件中查找到。
* 宏定义的意义在于,比如它可以指定 extern ”C“ 内部的函数采用 C 语言的命名风格来编译。(C 和 C++ 编译过程中对函数的命名风格不同,这将导致 JNI 在链接时无法根据函数名查找到具体的函数,那么 JNI 调用就无法完成)
*/
JNIEXPORT void JNICALL Java_com_example_testapplication_MediaRecorder_start
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

当在 Java 中调用 native_init() 时,会从 JNI 中寻找 Java_com_example_testapplication_MediaRecorder_native_1init 函数,如果没有就会报错,如果找到就会为这两个方法建立关联,其实是保存 JNI 的函数指针,这样再次调用 native_init() 时直接使用这个函数指针就可以了。

静态注册就是根据方法名,将 Java 方法和 JNI 函数建立关联,但是它有一些缺点:

  • JNI 层的函数名称过长。
  • 声明 Native 方法的类需要用 javah 生成头文件。
  • 初次调用 Native 方法时需要建立关联,影响效率。

静态注册就是 Java 的 Native 方法通过方法指针来与 JNI 进行关联,如果 Java 的 Native 方法知道它在 JNI 中对应的函数指针,就可以避免上述的缺点,这就是动态注册。动态注册要比静态注册复杂一些,但是一劳永逸。

动态注册

JNI 中有一种结构用来记录 Java 的 Native 和 JNI 方法的关联关系,它就是 JNINativeMethod,它在 jni.h 中被定义:

1
2
3
4
5
typedef struct{
const char* name; // Java 方法的名字
const char* signature; // Java 方法的签名信息
void* fnPtr; // JNI 中对应的方法指针
}JNINativeMethod;

系统的 MediaRecorder 采用的就是动态注册,查看它的 JNI 层实现:

1
2
3
4
5
6
7
8
9
// 定义了一个 JNINativeMethod 类型的数组,里面存储的就是 MediaRecorder 的 Native 方法与 JNI 层函数的对应关系。
static const JNINativeMethod gMethods[] = {
...
// Java 层的 Native 方法 , start 方法的签名信息 , 对应的 JNI 层函数
{"start", "()V", (void *)android_media_MediaRecorder_start},
{"stop", "()V", (void *)android_media_MediaRecorder_stop},
...
...
}

定义了 JNINativeMethod 类型的数组后,还需要注册它,注册的函数为 register_android_media_MediaRecorder,这个函数被调用在 android_media_MediaPlayer.cpp 的 JNI_OnLoad 函数中。

因为多媒体框架中很多都要进行 JNINativeMethod 数组注册,所以整个多媒体框架的注册函数被统一定义在 android_media_MediaPlayer.cpp 的 JNI_OnLoad 函数中, JNI_OnLoad 函数会在调用 System.loadLibrary 函数后调用。

register_android_media_MediaRecorder 函数通过一系列的方法调用,最终在 JNI 帮助类 JNIHelp.cpp 中通过调用 JNIEnv 的 RegisterNatives 函数完成 JNI 的注册。(JNIEnv 在 JNI 中十分重要)

数据类型转换

Java 的数据类型到了 JNI 层需要转换为 JNI 层的数据类型。Java 的数据类型分为基本数据类型和引用数据类型,JNI 层对于这两种类型也做了区分。

基本数据类型的转换

基本数据类型转换,除了最后一行的 void,其他的数据类型只需要在前面加上 “j” 就可以了。第三列的 Signature 代表签名格式。

引用数据类型的转换

从表 9-2 可以看出,数组的 JNI 层数据类型需要以 ”Array“ 结尾,签名格式的开头都会有 ”[“ 。需要注意有些数据类型的签名以 ”;“ 结尾。引用数据类型还具有继承关系,如图 9-3 所示,从图中看出 jclass、jstring、jarray 和 jthrowable 都继承 jobject,而 jobjectArray、jintArray 和 jlongArray 等类型都继承 jarray。

方法签名

方法签名是由签名格式(Signature)组成的,它的作用在于:

因为 Java 是有重载方法的,可以定义方法名相同,但参数不同的方法,正因如此,在 JNI 中仅仅通过方法名是无法找到 Java 中对应的具体方法的,所以 JNI 将参数类型和返回值类组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的 Java 方法,JNI 的方法签名的格式为:

(参数签名格式 … )返回值签名格式

手动组织方法签名麻烦且容易出错,Java 提供了 javap 命令来自动生成方法签名,在上面的例子中添加 native_setup 方法:

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.testapplication;

public class MediaRecorder {

static {
System.loadLibrary("media_jni");
native_init();
}

private static native final void native_init();
private native final void native_setup(Object mediarecorder_this,String clientName,String opPackageName) throws IllegalAccessException;
}

cd 到 testapplication 目录下,执行命令:

1、首先执行 javac MediaRecorder.java:生成 .class 文件

2、然后执行 javap -s -p MediaRecorder.class(s 表示输出内部类型签名,p 表示打印出所有的方法和成员(默认打印 public 成员))

3、最终在终端内的打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jianghouren@localhost testapplication % javap -s -p MediaRecorder.class
Compiled from "MediaRecorder.java"
public class com.example.testapplication.MediaRecorder {
public com.example.testapplication.MediaRecorder();
descriptor: ()V

private static final native void native_init();
descriptor: ()V

private final native void native_setup(java.lang.Object, java.lang.String, java.lang.String) throws java.lang.IllegalAccessException;
// native_setup() 在 JNI 中的方法签名
descriptor: (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V

static {};
descriptor: ()V
}
jianghouren@localhost testapplication %

解析 JNIEnv

JNIEnv 是 Native 世界中 Java 环境的代表,通过 JNIEnv * 指针就可以在 Native 世界中访问 Java 世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递,因此不同线程的 JNIEnv 是彼此独立的,JNIEnv 的主要作用有以下两点:

  • 调用 Java 的方法。
  • 操作 Java(操作 Java 中的变量和对象等)。

在 JNIEnv 的定义中,没有定义就是 C 代码,如果定义了就是 C++ 代码。

libnativehelper/include/nativehelper/jni.h
1
2
3
4
5
6
7
8
// 使用预定义宏 _cplusplus 来区分 C 和 C++ 两种代码
#if defined(_cplusplus)
typedef _JNIEnv JNIEnv; // C++ 中 JNIEnv 的类型是 JNIEnv
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv; // C 中 JNIEnv 的类型是 JNINativeInterface*
typedef const struct JNIInvokeInterface* JavaVm;
#endif

实际上,无论是 C 还是 C++,JNIEnv 的类型都和 JNINativeInterface 结构有关。查看源代码中关于它们的定义(8.0 版本):

libnativehelper/include/nativehelper/jni.h
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
31
32
33
34
35
/**
* _JNIEnv 是一个结构体,其内部又包含了 JNINativeInterface。
* 在 _JNIEnv 定义了很多函数,这里列举了 3 个比较常用的函数:
* FindClass:用来找到 Java 中指定名称的类。
* GetMethodID:用来得到 Java 中的方法。
* GetFieldID:用来得到 Java 中的成员变量。
* 在这三个函数中都调用了 JNINativeInterface 中定义的函数,所以说 JNIEnv 的类型都和 JNINativeInterface * 结构有关
*/
struct _JNIEnv {
const struct JNINativeInterface* functions;

#if defined(__cplusplus)
...
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
...
/**
* @clazz:Java 类
* @name:成员方法的名字
* @sig:这个成员方法的签名
*/
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
...
/**
* @clazz:Java 类
* @name:成员变量的名字
* @sig:这个成员变量的签名
*/
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }

...
#endif
};
libnativehelper/include/nativehelper/jni.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 在 JNINativeInterface 结构中定义了很多和 JNIEnv 结构体对应的函数指针,比如下面的三个函数指针定义。
* 通过这些函数指针的定义,就能够定位到虚拟机中的 JNI 函数表,从而实现了 JNI 层在虚拟机中的函数调用,
* 这样 JNI 层就可以调用 Java 世界的方法了。
*/
struct JNINativeInterface {
...
jclass (*FindClass)(JNIEnv*, const char*);
...
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
...
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
...
};

JavaVM 是虚拟机在 JNI 层的代表,在一个虚拟机进程中只有一个 JavaVM,因此,该进程的所有线程都可以使用这个 JavaVM。通过 JavaVM 的 AttachCurrentThread 函数可以获取这个线程的 JNIEnv,这样就可以在不同的线程中调用 Java 方法了。还要记得在使用 AttachCurrentThread 函数的线程退出前,务必要调用 DetachCurrentThread 函数来释放资源。

jfieldID 和 jmethodID

在 _JNIEnv 结构体中定义了很多函数,这些函数都会有不同的返回值,以上面例子中的 GetFieldID 和 GetMethodID 为例,这两个函数的返回值分别为 jfieldID 和 jmethodID。(当然还有一些其它函数的其它返回值)

接下来查看 MediaRecorder 框架的 JNI 层是如何使用 GetFieldID 和 GetMethodID 这两个方法的:

frameworks/base/media/jni/android_media_MediaRecorder.cpp
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
31
32
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
// 通过 FindClass 来找到 Java 层的 MediaRecorder 的 Class 对象,并赋值给 jclass 类型的变量 clazz,因此,clazz 就是 Java 层的 MediaRecorder 在 JNI 层的代表。
clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}
// fields 的定义如下段代码。
// 找到 Java 层的 MediaRecorder 中名为 mNativeContext 的成员变量,并赋值给 context。
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}
// 找到 Java 层的 MediaRecorder 中名为 mSurface 的成员变量,并赋值给 surface。
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;
}
// 获取 Java 层的 MediaRecorder 中名为 postEventFromNative 的静态方法,并赋值给 fields.post_event。
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");
if (fields.post_event == NULL) {
return;
}
}

fields 的定义如下:

frameworks/base/media/jni/android_media_MediaRecorder.cpp
1
2
3
4
5
6
7
struct fields_t {
jfieldID context;
jfieldID surface;

jmethodID post_event;
};
static fields_t fields;

将这些成员变量和方法赋值给 jfieldID 和 jmethodID 类型的变量有两个原因:

  • 为了效率考虑,如果每次调用相关方法时都要查询方法和变量,显然会效率很低。
  • 这些成员变量和方法都是本地引用,在 android_media_MediaRecorder_native_init 函数返回时这些本地引用会被自动释放,因此用 fields 来进行保存,以便后续使用。

综合上面两方面原因,在 MediaRecorder 框架 JNI 层的初始化方法 android_media_MediaRecorder_native_init 中将这些 jfieldID 和 jmethodID 类型的变量保存起来,是为了更高效率地供后续使用。

使用 jfieldID 和 jmethodID

保存了 jfieldID 和 jmethodID 类型的变量,接下来看如何使用它们。

首先查看如何使用了 jmethodID:

frameworks/base/media/jni/android_media_MediaRecorder.cpp
1
2
3
4
5
6
7
8
void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{
ALOGV("JNIMediaRecorderListener::notify");

JNIEnv *env = AndroidRuntime::getJNIEnv();
// 调用了 JNIEnv 的 CallStaticVoidMethod 函数,其中就传入了 fields.post_event,这个 fields.post_event 其实就是保存了 Java 层 MediaRecorder 的静态方法 postEventFromNative。
env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);
}

在 postEventFromNative 的内部会创建一个 Message 消息,并将它发送给 MediaRecorder 内部类 mEventHandler 来处理,这样做的目的是将代码逻辑运行在应用程序的主线程中。

JNIEnv 的 CallStaticVoidMethod 函数可以访问静态方法,如果想要访问 Java 的方法则可以使用 JNIEnv 的 CallVoidMethod 函数。

接下来查看如何使用了 jfieldID:

frameworks/base/media/jni/android_media_MediaRecorder.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void
android_media_MediaRecorder_prepare(JNIEnv *env, jobject thiz)
{
ALOGV("prepare");
sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
if (mr == NULL) {
jniThrowException(env, "java/lang/IllegalStateException", NULL);
return;
}
// 调用了 JNIEnv 的 GetObjectField 函数,fields.surface 是 jfieldID 类型的变量,用来保存 Java 层 MediaRecorder 中的成员变量 mSurface,mSurface 的类型为 Surface,这样通过 GetObjectField 函数就得到了 mSurface 在 JNI 层中对应的 jobject 类型变量 surface。
jobject surface = env->GetObjectField(thiz, fields.surface);
if (surface != NULL) {
const sp<Surface> native_surface = get_surface(env, surface);

...
}
process_media_recorder_call(env, mr->prepare(), "java/io/IOException", "prepare failed.");
}

引用类型

和 Java 的引用类型一样,JNI 也有引用类型,它们分别是本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Global References)。

本地引用

JNIEnv 提供的函数所返回的引用基本上都是本地引用,因此本地引用也是 JNI 中最常见的引用类型。本地引用主要有以下几个特点:

  • 当 Native 函数返回时,这个本地引用就会被自动释放。
  • 只在创建它的线程中有效,不能够跨线程使用。
  • 局部引用是 JVM 负责的引用类型,受 JVM 管理。

以 android_media_MediaRecorder_native_init 函数举例:

frameworks/base/media/jni/android_media_MediaRecorder.cpp
1
2
3
4
5
6
7
8
9
10
11
12
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
// 这里的 clazz 就是本地引用,它会在 android_media_MediaRecorder_native_init 函数调用返回后被自动释放。
// 也可以使用 JNIEnv 的 DeleteLocalRef 函数来手动删除本地引用,DeleteLocalRef 函数的使用场景主要是在 native 函数返回前占用了大量内存,需要调用 DeleteLocalRef 函数立即删除本地引用。
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}
...
}

全局引用

它和本地引用几乎是相反的,它主要有以下特点:

  • 在 native 函数返回时不会被自动释放,因此全局引用需要手动来进行释放,并且不会被 GC 回收。
  • 全局引用是可以跨线程使用的。
  • 全局引用不受到 JVM 管理。

JNIEnv 的 NewGlobalRef 函数用来创建全局引用,调用 JNIEnv 的 DeleteGlobalRef 函数来释放全局引用。

frameworks/base/media/jni/android_media_MediaRecorder.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
JNIMediaRecorderListener::JNIMediaRecorderListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{

jclass clazz = env->GetObjectClass(thiz);
if (clazz == NULL) {
ALOGE("Can't find android/media/MediaRecorder");
jniThrowException(env, "java/lang/Exception", NULL);
return;
}
// 调用 NewGlobalRef 函数将本地引用 clazz 转变为全局引用 mClass。
mClass = (jclass)env->NewGlobalRef(clazz);

mObject = env->NewGlobalRef(weak_thiz);
}

全局引用 mClass 的释放时机,查看 JNIMediaRecorderListener 的析构函数;

frameworks/base/media/jni/android_media_MediaRecorder.cpp
1
2
3
4
5
6
7
8
// 此析构函数用来释放全局引用
JNIMediaRecorderListener::~JNIMediaRecorderListener()
{
// remove global references
JNIEnv *env = AndroidRuntime::getJNIEnv();
env->DeleteGlobalRef(mObject);
env->DeleteGlobalRef(mClass); // 释放了全局引用 mClass
}

弱全局引用

弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,不同的是弱全局引用是可以被 GC 回收的,弱全局引用被 GC 回收之后会指向 NULL。

JNIEnv 的 NewWeakGlobalRef 函数用来创建弱全局引用,调用 JNIEnv 的 DeleteWeakGlobalRef 函数来释放弱全局引用。

由于弱全局引用可能被 GC 回收,因此在使用它之前要先判断它是否被回收了,方法就是调用 JNIEnv 的 IsSameObject 函数来判断;

1
2
3
4
5
6
// weakGlobalRef 是一个弱全局引用,使用它之前需要调用 JNIEnv 的 IsSameObject 函数来判断,这个函数会判断传入的两个引用是否相等,如果相等返回 JNI_TRUE,不相等返回 JNI_FALSE。
if(env->IsSameObject(weakGlobalRef,NULL)){
// 在这里,如果返回 JNI_TRUE 说明 weakGlobalRef 等于 NULL 并被 GC 回收了,就会调用 return false 不执行后面的代码逻辑。
return false;
}
... 使用 weakGlobalRef

备注

参考资料

Android 进阶解密(基于 8.0 系统)

Android 开发艺术探索

JNI/NDK 开发指南