如何使用JNI

JNI

JNI是Java与C/C++连接的桥梁,是通过动态库的动态装载机制来实现的。(在前面将静态库和动态库的时候,讲过动态库的动态装载)
对于动态装载涉及到两个核心的函数:

  • dlopen(): 打开一个动态库,并创建一个引用该动态库的句柄
  • dlsym(): 根据动态库的句柄以及函数符号,返回该符号对应函数的地址,从而可以执行该函数

在Java中一般通过System类的loadLibrary函数来加载一个动态库,该函数底层就是通过调用dlopen函数来实现的。

1
System.loadLibrary(libraryName);

JNI函数注册

注册JNI方法有两种方式,一种是静态注册,一种是动态注册。

动态注册

因为JNI允许我们提供一个函数映射表(native函数和jni函数对应表)。而在执行System.loadLibrary加载so库时,会执行该so的JNI_OnLoad函数,利用这个时机可以动态注册JIN方法。具体实现的话是通过JNINativeMethod结构保存映射关系,然后通过RegisterNatives函数来将该映射关系注册。

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 const JNINativeMethod gMethods[] = {
{"native_init", "()V",
(void *)com_zhenyu_martlet_MartletFormatEngine_native_init},
{"native_setup", "(Ljava/lang/Object;)V",
(void *)com_zhenyu_martlet_MartletFormatEngine_native_setup},
{"native_demuxer",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J",
(void *)com_zhenyu_martlet_MartletFormatEngine_native_demuxer},
{"native_muxer",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J",
(void *)com_zhenyu_martlet_MartletFormatEngine_native_muxer},
{"native_release", "()V",
(void *)com_zhenyu_martlet_MartletFormatEngine_native_release},
};

jint register_com_zhenyu_martlet_MartletFormatEngine(JNIEnv *env) {
jclass clazz;
// 找到native函数所在的类
clazz = env->FindClass("com/zhenyu/martlet/format/MartletFormatEngine");
if (clazz == NULL) {
return JNI_FALSE;
}
int numMethods = (sizeof(gMethods) / sizeof((gMethods)[0]));
// 注册映射关系
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
env->DeleteLocalRef(clazz);
return JNI_FALSE;
}
env->DeleteLocalRef(clazz);
return JNI_TRUE;
}

静态注册

如果没有在JNI_OnLoad中将JNI方法注册(将方法在进程中的地址增加到ClassObject->directMethods中),则在调用的时候解析javah风格的函数(比如Java_com_example_hellojni_HelloJni_stringFromJNI),进行静态注册。静态注册根据函数名来建立java方法和jni函数的对应关系。静态注册需要根据方法名本地搜索,比较耗时。

JNI线程

对于JNI来说,有两个比较重要的对象,一个是JavaVM,另一个是JNIEnv。其中JavaVM代表着JVM虚拟机,一个进程只有一个,可以通过JNI_OnLoad函数获取。而JNIEnv封装了JNI提供的所有API,是JNI执行的环境,一个线程只能有一个。

对于Java层的每一次JNI调用,都会通过JavaVM在当前线程中创建一个JNIEnv,当JNI调用执行完成时,会通过JavaVM销毁当前线程中的JNIEnv。

所以对于C++中手动创建的线程,默认是无法调用JNI方法的,需要通过JavaVM的AttachCurrentThread函数为当前线程创建一个JNIEnv后,才可以调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jint JNIHelper::attachThreadJNIEnv(JNIEnv **p_env) {
JavaVM *jvm = g_jvm;
JNIEnv *env = NULL;
// 如果当前线程已经有JNIEnv,则会返回JNI_OK
jint status = jvm->GetEnv((void **)&env, JNI_VERSION_1_6);
if (status == JNI_OK) {
*p_env = env;
MLOG("Already AttachCurrentThread");
return 0;
}

// 为当前线程创建JNIEnv
if (jvm->AttachCurrentThread(&env, NULL) == JNI_OK) {
MLOG("AttachCurrentThread");
*p_env = env;
return 0;
}
return -1;
}

当C++线程执行完成的时候,一定要手动的销毁JNIEnv,否则会造成内存泄露。

1
2
3
4
5
6
7
8
9
void JNIHelper::detachThreadJNIEnv() {
JavaVM *jvm = g_jvm;

if (jvm->DetachCurrentThread() == JNI_OK) {
MLOG("DetachCurrentThread");
return;
}
return;
}

JNI引用

对于JNI来说,引用类型分为Local引用、Global引用、Weak Global引用。(这里的引用都是指JNI类型的变量)

Local引用

对于Local引用是无法夸线程使用的,其生命周期在Native方法执行完成后,就会被结束。每次JNI调用的时候,JVM都会分配一块内存给当前线程,用于创建一个Local引用表,在native方法中创建的所有JNI类型变量都会保存在该表中。对于这些变量,当Native方法结束时,JNI会自动释放,也可以手动调用DeleteLocalRef函数来删除。

Global引用

当一个JNI类型的对象需要跨线程使用时,就需要将其转换为Global引用,通过NewGlobalRef函数。对于Global引用,GC是不会回收其内存,只能手动调用DeleteGlobalRef函数来释放。

Weak Global引用

如果一个对象想要进行跨线程使用,但是又想让GC可以回收,那么可以使用Weak Global引用。可以通过NewWeakGlobakRef和DeleteWeakGlobalRef函数来创建和删除。

在使用的时候需要注意一点,由于其GC的时机是不确定的,所以最好先通过NewLocalRef将Weak Global引用转换成Local引用再使用。