如何使用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 | // 定义映射关系 |
静态注册
如果没有在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 | jint JNIHelper::attachThreadJNIEnv(JNIEnv **p_env) { |
当C++线程执行完成的时候,一定要手动的销毁JNIEnv,否则会造成内存泄露。
1 | void JNIHelper::detachThreadJNIEnv() { |
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引用再使用。