🔥 Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook[1] 中。
这篇文章是 NDK 系列文章第 5 篇,专栏文章列表:
一、语言基础:
二、NDK 开发:
三、基础理论
四、计算机基础
JNI 学习路线图:
JNI(Java Native Interface,Java 本地接口)是 Java 生态的特性,它扩展了 Java 虚拟机的能力,使得 Java 代码可以与 C/C++ 代码进行交互。 通过 JNI 接口,Java 代码可以调用 C/C++ 代码,C/C++ 代码也可以调用 Java 代码。
这就引出第 1 个问题(为什么要这么做):Java 为什么要调用 C/C++ 代码,而不是直接用 Java 开发需求呢?我认为主要有 4 个原因:
还有第 2 个问题(为什么可以这么做):为什么两种独立的语言可以实现交互呢?因为 Java 虚拟机本身就是 C/C++ 实现的,无论是 Java 代码还是 C/C++ 代码,最终都是由这个虚拟机支撑,共同使用一个进程空间。JNI 要做的只是在两种语言之间做桥接。
一个标准的 JNI 开发流程主要包含以下步骤:
HelloWorld.java
,并声明 native 方法 sayHi();HelloWorld.class
字节码文件;HelloWorld.h
头文件(头文件中包含了本地方法的函数原型);HelloWorld.cpp
中实现函数原型;Hello-World.so
动态原生库文件;该流程用示意图表示如下:
JNI 本身本身并不能解决性能问题,错误地使用 JNI 反而可能引入新的性能问题,这些问题都是要注意的:
Java 的 native 方法和 JNI 函数是一一对应的映射关系,建立这种映射关系的注册方式有 2 种:
JNINativeMethod
结构体建立映射关系。关于注册 JNI 函数的更多原理分析,见 注册 JNI 函数。
so 库需要在运行时调用 System.loadLibrary(…)
加载,一般有 2 种调用时机:
关于加载 so 库的更多原理分析,见 so 文件加载过程分析[5]。
本节我们通过一个简单的 HelloWorld 程序来帮助你熟悉 JNI 的模板代码。
JNI Demo
JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi (JNIEnv *, jobject);
为什么 JNI 函数名要采用 Java_com_xurui_HelloWorld_sayHi
的命名方式呢?—— 这是 JNI 函数静态注册约定的函数命名规则。Java 的 native 方法和 JNI 函数是一一对应的映射关系,而建立这种映射关系的注册方式有 2 种:静态注册 + 动态注册。
其中,静态注册是基于命名约定建立的映射关系,一个 Java 的 native 方法对应的 JNI 函数会采用约定的函数名,即 Java_[类的全限定名 (带下划线)]_[方法名]
。JNI 调用 sayHi()
方法时,就会从 JNI 函数库中寻找函数 Java_com_xurui_HelloWorld_sayHi()
,更多内容见 注册 JNI 函数。
JNIEXPORT
是宏定义,表示一个函数需要暴露给共享库外部使用时。JNIEXPORT 在 Window 和 Linux 上有不同的定义:
jni.h
// Windows 平台 :
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
// Linux 平台:
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
JNICALL
是宏定义,表示一个函数是 JNI 函数。JNICALL 在 Window 和 Linux 上有不同的定义:
jni.h
// Windows 平台 :
#define JNICALL __stdcall // __stdcall 是一种函数调用参数的约定 ,表示函数的调用参数是从右往左。
// Linux 平台:
#define JNICALL
jobject
类型是 JNI 层对于 Java 层应用类型对象的表示。每一个从 Java 调用的 native 方法,在 JNI 函数中都会传递一个当前对象的引用。区分 2 种情况:
jclass
类型,指向 native 方法所在类的 Class 对象;jobject
类型,指向调用 native 方法的对象。JavaVM
和 JNIEnv
是定义在 jni.h 头文件中最关键的两个数据结构:
JavaVM 和 JNIEnv 的类型定义在 C 和 C++ 中略有不同,但本质上是相同的,内部由一系列指向虚拟机内部的函数指针组成。 类似于 Java 中的 Interface 概念,不同的虚拟机实现会从它们派生出不同的实现类,而向 JNI 层屏蔽了虚拟机内部实现(例如在 Android ART 虚拟机中,它们的实现分别是 JavaVMExt 和 JNIEnvExt)。
jni.h
struct _JNIEnv;
struct _JavaVM;
#if defined(__cplusplus)
// 如果定义了 __cplusplus 宏,则按照 C++ 编译
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
// 按照 C 编译
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
/*
* C++ 版本的 _JavaVM,内部是对 JNIInvokeInterface* 的包装
*/
struct _JavaVM {
// 相当于 C 版本中的 JNIEnv
const struct JNIInvokeInterface* functions;
// 转发给 functions 代理
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
...
};
/*
* C++ 版本的 JNIEnv,内部是对 JNINativeInterface* 的包装
*/
struct _JNIEnv {
// 相当于 C 版本的 JavaVM
const struct JNINativeInterface* functions;
// 转发给 functions 代理
jint GetVersion()
{ return functions->GetVersion(this); }
...
};
可以看到,不管是在 C 语言中还是在 C++ 中,JNINativeInterface*
和 JNINativeInterface*
这两个结构体指针才是 JavaVM 和 JNIEnv 的实体。不过 C++ 中加了一层包装,在语法上更简洁,例如:
示例程序
// 在 C 语言中,要使用 (*env)->
// 注意看这一句:typedef const struct JNINativeInterface* JNIEnv;
(*env)->FindClass(env, "java/lang/String");
// 在 C++ 中,要使用 env->
// 注意看这一句:jclass FindClass(const char* name)
//{ return functions->FindClass(this, name); }
env->FindClass("java/lang/String");
后文提到的大量 JNI 函数,其实都是定义在 JNINativeInterface 和 JNINativeInterface 内部的函数指针。
jni.h
/*
* JavaVM
*/
struct JNIInvokeInterface {
// 一系列函数指针
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
/*
* JNIEnv
*/
struct JNINativeInterface {
// 一系列函数指针
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);
jclass (*FindClass)(JNIEnv*, const char*);
...
};
这一节我们来讨论 Java 层与 Native 层之间的数据类型转换。
JNI 对于 Java 的基础数据类型(int 等)和引用数据类型(Object、Class、数组等)的处理方式不同。这个原理非常重要,理解这个原理才能理解后面所有 JNI 函数的设计思路:
另外需要特别注意一点,基础数据类型在映射时是直接映射,而不会发生数据格式转换。例如,Java char
类型在映射为 jchar
后旧是保持 Java 层的样子,数据长度依旧是 2 个字节,而字符编码依旧是 UNT-16 编码。
具体映射关系都定义在 jni.h
头文件中,文件摘要如下:
jni.h
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */ /* 注意:jchar 是 2 个字节 */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
typedef jint jsize;
#ifdef __cplusplus
// 内部的数据结构由虚拟机实现,只能从虚拟机源码看
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
...
// 说明我们接触到到 jobject、jclass 其实是一个指针
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
...
#else /* not __cplusplus */
...
#endif /* not __cplusplus */
我将所有 Java 类型与 JNI 类型的映射关系总结为下表:
Java 类型 | JNI 类型 | 描述 | 长度(字节) |
---|---|---|---|
boolean | jboolean | unsigned char | 1 |
byte | jbyte | signed char | 1 |
char | jchar | unsigned short | 2 |
short | jshort | signed short | 2 |
int | jint、jsize | signed int | 4 |
long | jlong | signed long | 8 |
float | jfloat | signed float | 4 |
double | jdouble | signed double | 8 |
Class | jclass | Class 类对象 | 1 |
String | jstrting | 字符串对象 | / |
Object | jobject | 对象 | / |
Throwable | jthrowable | 异常对象 | / |
boolean[] | jbooleanArray | 布尔数组 | / |
byte[] | jbyteArray | byte 数组 | / |
char[] | jcharArray | char 数组 | / |
short[] | jshortArray | short 数组 | / |
int[] | jinitArray | int 数组 | / |
long[] | jlongArray | long 数组 | / |
float[] | jfloatArray | float 数组 | / |
double[] | jdoubleArray | double 数组 | / |
上面提到 Java 对象会映射为一个 jobject 指针,那么 Java 中的 java.lang.String 字符串类型也会映射为一个 jobject 指针。可能是因为字符串的使用频率实在是太高了,所以 JNI 规范还专门定义了一个 jobject 的派生类 jstring
来表示 Java String 类型,这个相对特殊。
jni.h
// 内部的数据结构还是看不到,由虚拟机实现
class _jstring : public _jobject {};
typedef _jstring* jstring;
struct JNINativeInterface {
// String 转换为 UTF-8 字符串
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
// 释放 GetStringUTFChars 生成的 UTF-8 字符串
void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
// 构造新的 String 字符串
jstring (*NewStringUTF)(JNIEnv*, const char*);
// 获取 String 字符串的长度
jsize (*GetStringUTFLength)(JNIEnv*, jstring);
// 将 String 复制到预分配的 char* 数组中
void (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);
};
由于 Java 与 C/C++ 默认使用不同的字符编码,因此在操作字符数据时,需要特别注意在 UTF-16 和 UTF-8 两种编码之间转换。关于字符编码,我们在 Unicode 和 UTF-8 是什么关系? 这篇文章里讨论过,这里就简单回顾一下:
以下为 2 种较为常见的转换场景:
GetStringUTFChars
函数将一个 jstring 指针转换为一个 UTF-8 的 C/C++ 字符串,并在不再使用时调用 ReleaseStringChars
函数释放内存;NewStringUTF
函数构造一个新的 Java String 字符串对象。我们直接看一段示例程序:
示例程序
// 示例 1:将 Java String 转换为 C/C++ 字符串
jstring jStr = ...; // Java 层传递过来的 String
const char *str = env->GetStringUTFChars(jStr, JNI_FALSE);
if(!str) {
// OutOfMemoryError
return;
}
// 释放 GetStringUTFChars 生成的 UTF-8 字符串
env->ReleaseStringUTFChars(jStr, str);
// 示例 2:构造 Java String 对象(将 C/C++ 字符串转换为 Java String)
jstring newStr = env->NewStringUTF("在 Native 层构造 Java String");
if (newStr) {
// 通过 JNIEnv 方法将 jstring 调用 Java 方法(jstring 本身就是 Java String 的映射,可以直接传递到 Java 层)
...
}
此处对 GetStringUTFChars 函数的第 3 个参数 isCopy
做解释:它是一个布尔值参数,将决定使用拷贝模式还是复用模式:
另外还有一个基于范围的转换函数:GetStringUTFRegion
:预分配一块字符数组缓冲区,然后将 String 数据复制到这块缓冲区中。由于这个函数本身不会做任何内存分配,所以不需要调用对应的释放资源函数,也不会抛出 OutOfMemoryError
。另外,GetStringUTFRegion 这个函数会做越界检查并抛出 StringIndexOutOfBoundsException
异常。
示例程序
jstring jStr = ...; // Java 层传递过来的 String
char outbuf[128];
int len = env->GetStringLength(jStr);
env->GetStringUTFRegion(jStr, 0, len, outbuf);
与 jstring 的处理方式类似,JNI 规范将 Java 数组定义为 jobject 的派生类 jarray
:
jbooleanArray
、jintArray
等;jobjectArray
。下面区分基础类型数组和引用类型数组两种情况:
操作基础类型数组(以 jintArray 为例):
GetIntArrayElements
函数将一个 jintArray 指针转换为 C/C++ int 数组;ReleaseIntArrayElements
函数并使用模式 0;NewIntArray
函数构造 Java int 数组。我们直接看一段示例程序:
示例程序
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_xurui_hellojni_HelloWorld_generateIntArray(JNIEnv *env, jobject thiz, jint size) {
// 新建 Java int[]
jintArray jarr = env->NewIntArray(size);
// 转换为 C/C ++ int[]
int *carr = env->GetIntArrayElements(jarr, JNI_FALSE);
// 赋值
for (int i = 0; i < size; i++) {
carr[i] = i;
}
// 释放资源并回写
env->ReleaseIntArrayElements(jarr, carr, 0);
// 返回数组
return jarr;
}
此处重点对 ReleaseIntArrayElements 函数的第 3 个参数 mode
做解释:它是一个模式参数:
参数 mode | 描述 |
---|---|
0 | 将 C/C++ 数组的数据回写到 Java 数组,并释放 C/C++ 数组 |
JNI_COMMIT | 将 C/C++ 数组的数据回写到 Java 数组,并不释放 C/C++ 数组 |
JNI_ABORT | 不回写数据,但释放 C/C++ 数组 |
另外 JNI 还提供了基于范围函数:GetIntArrayRegion
和 SetIntArrayRegion
,使用方法和注意事项和 GetStringUTFRegion 也是类似的,也是基于一块预分配的数组缓冲区。
操作引用类型数组(jobjectArray):
SetObjectArrayElement
函数修改指定下标元素;FindClass
函数获取 Class 对象,再调用 NewObjectArray
函数构造对象数组。我们直接看一段示例程序:
示例程序
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_xurui_hellojni_HelloWorld_generateStringArray(JNIEnv *env, jobject thiz, jint size) {
// 获取 String Class
jclass jStringClazz = env->FindClass("java/lang/String");
// 初始值(可为空)
jstring initialStr = env->NewStringUTF("初始值");
// 创建 Java String[]
jobjectArray jarr = env->NewObjectArray(size, jStringClazz, initialStr);
// 赋值
for (int i = 0; i < size; i++) {
char str[5];
sprintf(str, "%d", i);
jstring jStr = env->NewStringUTF(str);
env->SetObjectArrayElement(jarr, i, jStr);
}
// 返回数组
return jarr;
}
这一节我们来讨论如何从 Native 层访问 Java 的字段与方法。在开始访问前,JNI 首先要找到想访问的字段和方法,这就依靠字段描述符和方法描述符。
在 Java 源码中定义的字段和方法,在编译后都会按照既定的规则记录在 Class 文件中的字段表和方法表结构中。例如,一个 public String str; 字段会被拆分为字段访问标记(public)、字段简单名称(str)和字段描述符(Ljava/lang/String)。因此,从 JNI 访问 Java 层的字段或方法时,首先就是要获取在 Class 文件中记录的简单名称和描述符。
Class 文件的一级结构:
字段表结构: 包含字段的访问标记、简单名称、字段描述符等信息。例如字段 String str
的简单名称为 str
,字段描述符为 Ljava/lang/String;
方法表结构: 包含方法的访问标记、简单名称、方法描述符等信息。例如方法 void fun();
的简单名称为 fun
,方法描述符为 ()V
Java 类型 | 描述符 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
floag | F |
double | D |
void | V |
引用类型 | 以 L 开头 ; 结尾,中间是 / 分隔的包名和类名。例如 String 的字段描述符为 Ljava/lang/String; |
void fun();
的简单名称为 fun
,方法描述符为 ()V
本地代码访问 Java 字段的流程分为 2 步:
Fid = env->GetFieldId(clz, "name", "Ljava/lang/String;");
Jstr = env->GetObjectField(thiz, Fid);
Java 字段分为静态字段和实例字段,相关方法如下:
示例程序
extern "C"
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessField(JNIEnv *env, jobject thiz) {
// 获取 jclass
jclass clz = env->GetObjectClass(thiz);
// 示例:修改 Java 静态变量值
// 静态字段 ID
jfieldID sFieldId = env->GetStaticFieldID(clz, "sName", "Ljava/lang/String;");
// 访问静态字段
if (sFieldId) {
// Java 方法的返回值 String 映射为 jstring
jstring jStr = static_cast<jstring>(env->GetStaticObjectField(clz, sFieldId));
// 将 jstring 转换为 C 风格字符串
const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);
// 释放资源
env->ReleaseStringUTFChars(jStr, sStr);
// 构造 jstring
jstring newStr = env->NewStringUTF("静态字段 - Peng");
if (newStr) {
// jstring 本身就是 Java String 的映射,可以直接传递到 Java 层
env->SetStaticObjectField(clz, sFieldId, newStr);
}
}
// 示例:修改 Java 成员变量值
// 实例字段 ID
jfieldID mFieldId = env->GetFieldID(clz, "mName", "Ljava/lang/String;");
// 访问实例字段
if (mFieldId) {
jstring jStr = static_cast<jstring>(env->GetObjectField(thiz, mFieldId));
// 转换为 C 字符串
const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);
// 释放资源
env->ReleaseStringUTFChars(jStr, sStr);
// 构造 jstring
jstring newStr = env->NewStringUTF("实例字段 - Peng");
if (newStr) {
// jstring 本身就是 Java String 的映射,可以直接传递到 Java 层
env->SetObjectField(thiz, mFieldId, newStr);
}
}
}
本地代码访问 Java 方法与访问 Java 字段类似,访问流程分为 2 步:
Mid = env->GetMethodID(jclass, "helloJava", "()V");
env->CallVoidMethod(thiz, Mid);
Java 方法分为静态方法和实例方法,相关方法如下:
示例程序
extern "C"
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessMethod(JNIEnv *env, jobject thiz) {
// 获取 jclass
jclass clz = env->GetObjectClass(thiz);
// 示例:调用 Java 静态方法
// 静态方法 ID
jmethodID sMethodId = env->GetStaticMethodID(clz, "sHelloJava", "()V");
if (sMethodId) {
env->CallStaticVoidMethod(clz, sMethodId);
}
// 示例:调用 Java 实例方法
// 实例方法 ID
jmethodID mMethodId = env->GetMethodID(clz, "helloJava", "()V");
if (mMethodId) {
env->CallVoidMethod(thiz, mMethodId);
}
}
访问 Java 层字段或方法时,需要先利用字段名 / 方法名和描述符进行检索,获得 jfieldID / jmethodID。这个检索过程比较耗时,优化方法是将字段 ID 和方法 ID 缓存起来,减少重复检索。
提示: 从不同线程中获取同一个字段或方法 的 ID 是相同的,缓存 ID 不会有多线程问题。
缓存字段 ID 和 方法 ID 的方法主要有 2 种:
JNI_OnLoad
方法中缓存,也可以在加载 so 库后调用一个 native 方法进行缓存。两种缓存 ID 方式的主要区别在于缓存发生的时机和时效性:
在讨论 JNI 中的对象引用管理,我们先回顾一下 Java 和 C/C++ 在对象内存回收上的区别:
而 JNI 层作为 Java 层和 C/C++ 层之间的桥接层,那么它就会兼具两者的特点:对于
NewObject
等函数创建 Java 对象,并且返回对象的引用,这个引用就是 Local 型的局部引用。对于局部引用,可以通过 DeleteLocalRef
函数手动显式释放(这类似于在 Java 中显式置空局部变量),也可以等到函数返回时自动释放(这类似于在 Java 中方法返回时隐式置空局部变量);NewGlobalRef
函数将局部引用升级为 Global 型全局变量,这样就可以在方法使用对象(这类似于在 Java 中使用 static 变量指向对象)。在不再使用对象时必须调用 DeleteGlobalRef
函数释放全局引用(这类似于在 Java 中显式置空 static 变量)。提示: 我们这里所说的 ”置空“ 只是将指向变量的值赋值为 null,而不是回收对象,Java 对象回收是交给垃圾回收器处理的。
DeleteLocalRef
函数手动释放;NewGlobalRef
函数创建,不再使用对象时必须通过 DeleteGlobalRef
函数释放。NewGlobalWeakRef
函数创建,不再使用对象时必须通过 DeleteGlobalWeakRef
函数释放。示例程序
// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
env->DeleteLocalRef(localRefClz);
// 全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
env->DeleteGlobalRef(globalRefClz);
// 弱全局引用
jclass weakRefClz = env->NewWeakGlobalRef(localRefClz);
env->DeleteGlobalWeakRef(weakRefClz);
在 JavaVM 和 JNIEnv 中,会分别建立多个表管理引用:
可以使用 JNI 函数 IsSameObject
判断两个引用是否指向相同对象(适用于三种引用类型),返回值为 JNI_TRUE
时表示相同,返回值为 JNI_FALSE
表示不同。例如:
示例程序
jclass localRef = ...
jclass globalRef = ...
bool isSampe = env->IsSamObject(localRef, globalRef)
另外,当引用与 NULL
比较时含义略有不同:
JNI 中的异常机制与 Java 和 C/C++ 的处理机制都不同:
throw
抛出异常,虚拟机会中断当前执行流程,转而去寻找匹配的 catch{} 块,或者继续向外层抛出寻找匹配 catch {} 块。ThrowNew
抛出异常,程序不会中断当前执行流程,而是返回 Java 层后,虚拟机才会抛出这个异常。因此,在 JNI 层出现异常时,有 2 种处理选择:
return
当前方法,让 Java 层去处理这个异常(这类似于在 Java 中向方法外层抛出异常);ExceptionClear
清除这个异常,再执行异常处理程序(这类似于在 Java 中 try-catch 处理异常)。需要注意的是,当异常发生时,必须先处理-清除异常,再执行其他 JNI 函数调用。因为当运行环境存在未处理的异常时,只能调用 2 种 JNI 函数:异常护理函数和清理资源函数。JNI 提供了以下与异常处理相关的 JNI 函数:
jni.h
struct JNINativeInterface {
// 抛出异常
jint (*ThrowNew)(JNIEnv *, jclass, const char *);
// 检查异常
jthrowable (*ExceptionOccurred)(JNIEnv*);
// 检查异常
jboolean (*ExceptionCheck)(JNIEnv*);
// 清除异常
void (*ExceptionClear)(JNIEnv*);
};
示例程序
// 示例 1:向 Java 层抛出异常
jclass exceptionClz = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(exceptionClz, "来自 Native 的异常");
// 示例 2:检查当前环境是否发生异常(类似于 Java try{})
jthrowable exc = env->ExceptionOccurred(env);
if(exc) {
// 处理异常(类似于 Java 的 catch{})
}
// 示例 3:清除异常
env->ExceptionClear();
异常处理的步骤我懂了,由于虚拟机在遇到 ThrowNew 时不会中断当前执行流程,那我怎么知道当前已经发生异常呢?有 2 种方法:
ExceptionOccurred
或 ExceptionCheck
检查当前是否有异常发生。这一节我们来讨论 JNI 层中的多线程操作。
在 JNI 中,有 2 类引用是无法跨线程调用的,必须时刻谨记:
AttachCurrentThread
函数将当前线程依附到 JavaVM 上,获得属于当前线程的 JNIEnv 指针。如果当前线程已经依附到 JavaVM,也可以直接使用 GetEnv 函数。示例程序
JNIEnv * env_child;
vm->AttachCurrentThread(&env_child, nullptr);
// 使用 JNIEnv*
vm->DetachCurrentThread();
示例程序
// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
// 释放全局引用(非必须)
env->DeleteLocalRef(localRefClz);
// 局部引用升级为全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
// 释放全局引用(必须)
env->DeleteGlobalRef(globalRefClz);
在 JNI 中也会存在多个线程同时访问一个内存资源的情况,此时需要保证并发安全。在 Java 中我们会通过 synchronized 关键字来实现互斥块(背后是使用监视器字节码),在 JNI 层也提供了类似效果的 JNI 函数:
IllegalMonitorStateException
异常。jni.h
struct JNINativeInterface {
jint (*MonitorEnter)(JNIEnv*, jobject);
jint (*MonitorExit)(JNIEnv*, jobject);
}
示例程序
// 进入监视器
if (env->MonitorEnter(obj) != JNI_OK) {
// 建立监视器的资源分配不成功等
}
// 此处为同步块
if (env->ExceptionOccurred()) {
// 必须保证有对应的 MonitorExit,否则可能出现死锁
if (env->MonitorExit(obj) != JNI_OK) {
...
};
return;
}
// 退出监视器
if (env->MonitorExit(obj) != JNI_OK) {
...
};
JNI 没有提供 Object 的 wati/notify 相关功能的函数,需要通过 JNI 调用 Java 方法的方式来实现:
示例程序
static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;
void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout) {
env->CallVoidMethod(object, MID_Object_wait, timeout);
}
void
JNU_MonitorNotify(JNIEnv *env, jobject object) {
env->CallVoidMethod(object, MID_Object_notify);
}
void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object) {
env->CallVoidMethod(object, MID_Object_notifyAll);
}
在 JNI 开发中,有两种创建线程的方式:
Thread#start()
可以创建线程,优点是可以方便地设置线程名称和调试;pthread_create()
或 std::thread
也可以创建线程示例程序
//
void *thr_fn(void *arg) {
printids("new thread: ");
return NULL;
}
int main(void) {
pthread_t ntid;
// 第 4 个参数将传递到 thr_fn 的参数 arg 中
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0) {
printf("can't create thread: %s\n", strerror(err));
}
return 0;
}
光说不练假把式,以下给出一个简单的 JNI 开发模板,将包括上文提到的一些比较重要的知识点。程序逻辑很简单:Java 层传递一个媒体文件路径到 Native 层后,由 Native 层播放媒体并回调到 Java 层。为了程序简化,所有真实的媒体播放代码都移除了,只保留模板代码。
start()
方法开始,调用 startNative()
方法进入 Native 层;onStarted()
方法。MediaPlayer.kt
// Java 层模板
class MediaPlayer {
companion object {
init {
// 注意点:加载 so 库
System.loadLibrary("hellondk")
}
}
// Native 层指针
private var nativeObj: Long? = null
fun start(path : String) {
// 注意点:记录 Native 层指针,后续操作才能拿到 Native 的对象
nativeObj = startNative(path)
}
fun release() {
// 注意点:使用 start() 中记录的指针调用 native 方法
nativeObj?.let {
releaseNative(it)
}
nativeObj = null
}
private external fun startNative(path : String): Long
private external fun releaseNative(nativeObj: Long)
fun onStarted() {
// Native 层回调(来自 JNICallbackHelper#onStarted)
...
}
}
native-lib.cpp
// 注意点:记录 JavaVM 指针,用于在子线程获得 JNIEnv
JavaVM *vm = nullptr;
jint JNI_OnLoad(JavaVM *vm, void *args) {
::vm = vm;
return JNI_VERSION_1_6;
}
extern "C"
JNIEXPORT jlong JNICALL
Java_com_pengxr_hellondk_MediaPlayer_startNative(JNIEnv *env, jobject thiz, jstring path) {
// 注意点:String 转 C 风格字符串
const char *path_ = env->GetStringUTFChars(path, nullptr);
// 构造一个 Native 对象
auto *helper = new JNICallbackHelper(vm, env, thiz);
auto *player = new MediaPlayer(path_, helper);
player->start();
// 返回 Native 对象的指针
return reinterpret_cast<jlong>(player);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_pengxr_hellondk_MediaPlayer_releaseNative(JNIEnv *env, jobject thiz, jlong native_obj) {
auto * player = reinterpret_cast<MediaPlayer *>(native_obj);
player->release();
}
JNICallbackHelper.h
#ifndef HELLONDK_JNICALLBACKHELPER_H
#define HELLONDK_JNICALLBACKHELPER_H
#include <jni.h>
#include "util.h"
class JNICallbackHelper {
private:
// 全局共享的 JavaVM*
// 注意点:指针要初始化 0 值
JavaVM *vm = 0;
// 主线程的 JNIEnv*
JNIEnv *env = 0;
// Java 层的对象 MediaPlayer.kt
jobject job;
// Java 层的方法 MediaPlayer#onStarted()
jmethodID jmd_prepared;
public:
JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job);
~JNICallbackHelper();
void onStarted();
};
#endif //HELLONDK_JNICALLBACKHELPER_H
JNICallbackHelper.cpp
#include "JNICallbackHelper.h"
JNICallbackHelper::JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job) {
// 全局共享的 JavaVM*
this->vm = vm;
// 主线程的 JNIEnv*
this->env = env;
// C 回调 Java
jclass mediaPlayerKTClass = env->GetObjectClass(job);
jmd_prepared = env->GetMethodID(mediaPlayerKTClass, "onPrepared", "()V");
// 注意点:jobject 无法跨越线程,需要转换为全局引用
// Error:this->job = job;
this->job = env->NewGlobalRef(job);
}
JNICallbackHelper::~JNICallbackHelper() {
vm = nullptr;
// 注意点:释放全局引用
env->DeleteGlobalRef(job);
job = nullptr;
env = nullptr;
}
void JNICallbackHelper::onStarted() {
// 注意点:子线程不能直接使用持有的主线程 env,需要通过 AttachCurrentThread 获取子线程的 env
JNIEnv * env_child;
vm->AttachCurrentThread(&env_child, nullptr);
// 回调 Java 方法
env_child->CallVoidMethod(job, jmd_prepared);
vm->DetachCurrentThread();
}
MediaPlayer.h
#ifndef HELLONDK_MEDIAPLAYER_H
#define HELLONDK_MEDIAPLAYER_H
#include <cstring>
#include <pthread.h>
#include "JNICallbackHelper.h"
class MediaPlayer {
private:
char *path = 0;
JNICallbackHelper *helper = 0;
pthread_t pid_start;
public:
MediaPlayer(const char *path, JNICallbackHelper *helper);
~MediaPlayer();
void doOpenFile();
void start();
void release();
};
#endif //HELLONDK_MEDIAPLAYER_H
MediaPlayer.cpp
#include "MediaPlayer.h"
MediaPlayer::MediaPlayer(const char *path, JNICallbackHelper *helper) {
// 注意点:参数 path 指向的空间被回收会造成悬空指针,应复制一份
// this->path = path;
this->path = new char[strlen(path) + 1];
strcpy(this->path, path);
this->helper = helper;
}
MediaPlayer::~MediaPlayer() {
if (path) {
delete path;
}
if (helper) {
delete helper;
}
}
// 在子线程执行
void MediaPlayer::doOpenFile() {
// 省略真实播放逻辑...
// 媒体文件打开成功
helper->onStarted();
}
// 在子线程执行
void *task_open(void *args) {
// args 是 主线程 MediaPlayer 的实例的 this变量
auto *player = static_cast<MediaPlayer *>(args);
player->doOpenFile();
return nullptr;
}
void MediaPlayer::start() {
// 切换到子线程执行
pthread_create(&pid_start, 0, task_open, this);
}
void MediaPlayer::release() {
...
}
到这里,JNI 的知识就讲完了,你可以按照学习路线图来看。下一篇,我们开始讲 Android NDK 开发。关注我,带你建立核心竞争力,我们下次见。
好的身体才是革命写代码的本钱!
[1]
GitHub · Android-NoteBook: https://github.com/pengxurui/Android-NoteBook
[2]
DemoHall·HelloJni: https://github.com/pengxurui/DemoHall
[3]
1、JNI 基础:Java 与 Native 交互(本文): https://juejin.cn/post/7125338583959306248
[4]
7、PNG 图片:无损压缩编码: https://juejin.cn/post/6905635070397612039
[5]
so 文件加载过程分析: https://juejin.cn/post/6892793299427491854
[6]
JNI 提示: https://developer.android.google.cn/training/articles/perf-jni
[7]
Java 原生接口规范: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
[8]
深入理解 Android:卷 1(第 2 章 · 深入理解 JNI): https://weread.qq.com/web/reader/e3d32fb0593388e3dde8006k9bf32f301f9bf31c7ff0a60
[9]
深入理解 Android:Java 虚拟机 ART(第 11 章 · ART 中的 JNI): https://weread.qq.com/web/reader/3ee32e60717f5af83ee7b37
[10]
Android 应用安全防护和逆向分析(基础篇): https://weread.qq.com/web/bookDetail/6ef32f805e0b836efa707cb
[11]
Java 性能权威指南:第 2 版(第 12.5 节:Java 原生接口): https://weread.qq.com/web/reader/f5d32af0729dd0d6f5d1789kf03328d0250f033ab37c722