美文网首页
JNI 基本入门

JNI 基本入门

作者: 你可记得叫安可 | 来源:发表于2020-09-01 09:12 被阅读0次

    使用 C 的一个简单 JNI

    编写 Java 层代码

    // HelloJNI.java
    public class HelloJNI {
        static {
            System.loadLibrary("hello");
        }
    
        private native void sayHello();
    
        public static void main(String[] args) {
            new HelloJNI().sayHello();
        }
    }
    
    • 我们通过 System.loadLibrary() 来加载一个名字叫 hello 的动态库。该动态库的文件名在 Win 系统上叫 hello.dll,在 Linux 系统上是 libhello.so,在 MacOS 上是 libhello.dylib。动态库需要被添加到 Java 的库索引目录下,才能被 System.loadLibrary() 找到。可以在打包时通过 VM 参数 -Djava.library.path=/path/to/lib 来添加动态库路径。

    Java8 以前,System.loadLibrary() 只支持加载动态库。但是在 Java8 及以后,支持连接静态库。

    生成 C 层头文件

    JDK8 开始,我们使用 javac -h 来编译出 .class 文件和生成 Java 文件中 native 的方法所对应的头文件:

    javac -h . HelloJNI.java
    

    生成出来的头文件 HelloJNI.h 如下所示:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class HelloJNI */
    
    #ifndef _Included_HelloJNI
    #define _Included_HelloJNI
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     HelloJNI
     * Method:    sayHello
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_HelloJNI_sayHello
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    该工程第一次生成该文件时,可能会提示找不到 jni.h 文件,原因和解决方法可以看:JNI 解惑。我们可以看到头文件声明了一个 C 函数 JNIEXPORT void JNICALL Java_HelloJNI_sayHello (JNIEnv *, jobject);。它的命名规则是 Java_{package_and_classname}_{function_name}(JNI_arguments)

    • 参数 JNIEnv 代表了 JNI 环境,通过它可以访问所有的 JNI 函数。
    • 参数 jobject 代表了调用该方法的上层 Java 实例。
    • extern "C" 则涉及到一个 C++ 的概念:Name Mangling

    JNIEXPORT & JNICALL:这两个宏定义的原理需要查看:when-to-use-jniexport-and-jnicall-in-android-ndk

    • JNIEXPORT 使 native 函数在动态表中可见,这样 JNI 才能找得到。如果不可见的话,那么在运行时调用 RegisterNatives 方法将会失败。
    • JNICALLAndroid 平台上是一个空定义,它主要是为了平台兼容性而定义的,在 Windows 平台上,这个宏被定义为 __stdcall

    编写 C 层代码 HelloJNI.c

    // "HelloJNI.c"
    #include <jni.h>        // JNI header provided by JDK
    #include <stdio.h>      // C Standard IO Header
    #include "HelloJNI.h"   // Generated
     
    // Implementation of the native method sayHello()
    JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
       printf("Hello World!\n");
       return;
    }
    

    下面我们使用 clang 来编译 libhello.dylib

    clang -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.c 
    

    上面的编译命令使我们能够编译出 libhello.dylib 动态库。然后再编译、运行 HelloJNI.java 即可得到输出。

    编译 java 程序:javac HelloJNI.java,运行:java HelloJNI。可能还需要指定 java 库的搜索路径:java -Djava.library.path=. HelloJNI
    之所以使用 clang 而不是 gcc,是因为在 NDK r11 之后,Android 默认使用 clang/clang++Why did you deprecate GCC?。而 clangclang++ 的主要区别就在于,clang 的默认编译选项是支持 C 语言的,而 clang++ 的默认编译选项是支持 C++ 的:What is the difference? clang++ | clang -std=c++11。他们的主要区别在于链接时,clang++ 会默认链接 c++ 标准库,其实用 clang 一样可以编译 C++ 代码,只需要加上 -lc++(for libc++) 或者 -lstdc++(for libstdc++)Difference-between-clang-and-clang

    编写 C++ 层代码 HelloJNI.cpp

    我们同样可以用 c++ 来写上面的 JNI 层代码:

    #include <jni.h>
    #include <iostream>
    #include "HelloJNI.h"
    using namespace std;
    
    JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
        cout << "Hello World from C++!" << endl;
        return;
    }
    

    使用 clang++ 编译:clang++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.cpp

    或者用 clang 编译:clang -x c++ -lc++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.cpp

    JNI 基本知识

    JNI 定义了一些自己的类型,它们与 java 的类型是一一映射的:

    1. 基本类型:jintjbytejshortjlongjfloatjdoublejcharjboolean,它们对应了 8 种 java 的基本类型:intbyteshortlongfloatdoublecharboolean
    2. 引用类型:jobject 对应了 java.lang.Object。还有 jobject 的子类型:
      a. jclass -> java.lang.Class
      b. jstring -> java.lang.String
      c. jthrowable -> java.lang.Throwable
      d. jarray 对应 java 数组。它代表了 8 种基本类型数组 和 一个 Object 数组:jintArrayjbyteArrayjshortArrayjlongArrayjfloatArrayjdoubleArrayjcharArrayjbooleanArrayjobjectArray

    jarrayjni.h 中的定义就能看出:

    typedef jarray jbooleanArray;
    typedef jarray jbyteArray;
    typedef jarray jcharArray;
    typedef jarray jshortArray;
    typedef jarray jintArray;
    typedef jarray jlongArray;
    typedef jarray jfloatArray;
    typedef jarray jdoubleArray;
    typedef jarray jobjectArray;
    

    JNInative 函数接受和返回都是上面的 JNI 类型,但是它所调用的 C/C++ 业务逻辑方法却是接受 C/C++ 的数据类型,因此 native 的函数本质上就是一个胶水层转换代码,工作就是把 java 层传下来的 JNI 类型转换为 C/C++ 类型后给业务代码调用。最后将业务代码返回的 C/C++ 类型转换为 JNI 类型再返回给 java 层。

    我们上面提到的 JNI 基本类型都是可以直接被 C/C++ 使用的:

    // jni_md.h
    typedef int jint;
    // jni.h
    typedef unsigned char   jboolean;
    typedef unsigned short  jchar;
    typedef short           jshort;
    typedef float           jfloat;
    typedef double          jdouble;
    
    typedef jint            jsize;
    

    因此主要的难点就在于引用类型的转换。下面就看下一些引用类型的转换。

    字符串 jstring 的转换

    public class TestJNIString {
        static {
           System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes), libmyjni.dylib (MacOS)
        }
        // Native method that receives a Java String and return a Java String
        private native String sayHello(String msg);
      
        public static void main(String args[]) {
           String result = new TestJNIString().sayHello("Hello from Java");
           System.out.println("In Java, the returned string is: " + result);
        }
     }
    

    javac -h . TestJNIString.java 生成 java 字节码和 C 层头文件。

    C 层的实现
    #include <jni.h>
    #include <stdio.h>
    #include "TestJNIString.h"
    
    JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
        // 第一步,将 JNI String (jstring) 转换成 C-String (char*)
        const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL);
        if (NULL == inCStr) return NULL;
    
        printf("In C, the received string is: %s\n", inCStr);
        // 释放 JNI 资源
        (*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr);
    
        // Prompt user for a C-string
        char outCStr[128];
        printf("Enter a String: ");
        scanf("%s", outCStr);
    
        // 将 C-String (char*) 转换成 JNI String (jstring)
        return (*env)->NewStringUTF(env, outCStr);
    }
    

    JNI 支持 UnicodeUTF-8 两种编码的字符串。UTF-8 字符串更像是 C-String(char 数组),大部分时候我们在 CC/C++ 代码中使用支持 UTF-8 编码的 JNI 函数:

    // 返回使用 utf-8 编码 string 后得到的字节数组指针
    const char* GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
    // 提示 VM 回收 utf 资源
    void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
    // 从一个 utf-8 编码的字节数组中,编码后返回 java.lang.String 对象
    jstring NewStringUTF(JNIEnv *env, const char *bytes);
    // 返回 utf-8 编码的字符串的长度
    jsize GetStringUTFLength(JNIEnv *env, jstring string);
    // 将 str 中从 start 开始的 length 个字符编码成 utf-8 字符串,并放进 buf 中
    void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
    
    • 上面代码有一个需要注意的地方:当你使用完 GetStringUTFChars() 返回的字符数组时,一定要调用 ReleaseStringUTFChars() 以使 JVM 能够回收资源。

    基本类型数组的转换

    public class TestJNIPrimitiveArray {
        static {
           System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes), libmyjni.dylib (MacOS)
        }
      
        // Declare a native method sumAndAverage() that receives an int[] and
        //  return a double[2] array with [0] as sum and [1] as average
        private native double[] sumAndAverage(int[] numbers);
      
        // Test Driver
        public static void main(String args[]) {
           int[] numbers = {22, 33, 33};
           double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers);
           System.out.println("In Java, the sum is " + results[0]);
           System.out.println("In Java, the average is " + results[1]);
        }
     }
    
    c 层的实现
    #include <jni.h>
    #include <stdio.h>
    #include "TestJNIPrimitiveArray.h"
    
    JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage(JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
        // 第一步:将传入进来的 JNI jintarray 转换成 C 的 jint[]
        jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
        if (NULL == inCArray) return NULL;
        jsize length = (*env)->GetArrayLength(env, inJNIArray);
    
        jint sum = 0;
        int i;
        for (i = 0; i < length; i++) {
            sum += inCArray[i];
        }
        jdouble average = (jdouble)sum / length;
        // 第二步:释放资源
        (*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0);
    
        jdouble outCArray[] = {sum, average};
    
        // 将 C 的 jdouble[] 转换成 jdoublearray
        jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2);
        if (NULL == outJNIArray) return NULL;
        (*env)->SetDoubleArrayRegion(env, outJNIArray, 0, 2, outCArray);
        return outJNIArray;
    }
    
    • 跟上面 字符串 jstring 的转换 一样有个需要注意的地方:使用 GetIntArrayElements() 分配出来的 jint[] 资源,需要用 ReleaseIntArrayElements() 回收。

    还有其他基本类型数组的 JNI 函数,这里不再分析。

    访问实例变量

    public class TestJNIInstanceVariable {
       static {
          System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
       }
     
       // Instance variables
       private int number = 88;
       private String message = "Hello from Java";
     
       // Declare a native method that modifies the instance variables
       private native void modifyInstanceVariable();
     
       // Test Driver   
       public static void main(String args[]) {
          TestJNIInstanceVariable test = new TestJNIInstanceVariable();
          test.modifyInstanceVariable();
          System.out.println("In Java, int is " + test.number);
          System.out.println("In Java, String is " + test.message);
       }
    }
    

    上面的代码将在 C 层改变实例的私有变量值。

    C 层代码实现
    #include <jni.h>
    #include <stdio.h>
    #include "TestJNIInstanceVariable.h"
    
    JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable(JNIEnv *env, jobject thisObj) {
        // 获取到实例 class 的引用
        jclass thisClass = (*env)->GetObjectClass(env, thisObj);
    
        // 获取到属性 number 的 id
        jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
        if (NULL == fidNumber) return;
    
        // 从实例 thisObj 中根据属性 id 获取到属性的具体值
        jint number = (*env)->GetIntField(env, thisObj, fidNumber);
        printf("In C, the int is %d\n", number);
    
        // 将新的值设置给实例 thisObj 中 id 为 fidNumber 的属性(即使这个属性是 private 的)
        number = 99;
        (*env)->SetIntField(env, thisObj, fidNumber, number);
    
        jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
        if (NULL == fidMessage) return;
    
        jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);
    
        const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
        if (NULL == cStr) return;
    
        printf("In C, the string is %s\n", cStr);
        (*env)->ReleaseStringUTFChars(env, message, cStr);
    
        message = (*env)->NewStringUTF(env, "Hello from C");
        if (NULL == message) return;
    
        (*env)->SetObjectField(env, thisObj, fidMessage, message);
    }
    
    1. 通过 GetObjectClass() 获取类的 jclass
    2. 通过 GetFieldID() 获取需要访问的属性 id。这个方法需要传入一个属性的签名 或 描述符。

    java 中,类的描述符格式为 "L<fully-qualified-name>;",需要将全限定名中的 '.' 换成 '/',如 String -> "Ljava/lang/String;"。对于基本类型,"I" -> int,"B" -> byte,"S" -> short,"J" -> long,"F" -> float,"D" -> double,“C” -> char,"Z" -> boolean。对于数组,则加一个前缀 "[",如 "[Ljava/lang/Object" -> Object[];"[I" -> int[]

    1. 通过属性 id,调用 GetObjectField() 或者 Get<primitive-type>Field() 方法获取该属性在当前实例中的值。
    2. 通过属性 id,调用 SetObjectField() 或者 Set<primitive-type>Field() 方法重新设置该属性在当前实例中的值。

    访问类静态变量

    方法跟上一节中 访问实例变量 差不多,只不过访问方法变成了 GetStaticFieldID()Get|SetStaticObjectField()Get|SetStatic<Primitive-type>Field()

    回调实例方法 和 实例静态方法

    public class TestJNICallBackMethod {
        static {
            System.loadLibrary("myjni");
        }
    
        private native void nativeMethod();
    
        // To be called back by the native code
        private void callback() {
            System.out.println("In Java");
        }
    
        private void callback(String message) {
            System.out.println("In Java with " + message);
        }
    
        private double callbackAverage(int n1, int n2) {
            return ((double)n1 + n2) / 2.0;
        }
    
        // Static method to be called back
        private static String callbackStatic() {
            return "From static Java method";
        }
    
        public static void main(String[] args) {
            new TestJNICallBackMethod().nativeMethod();
        }
    }
    

    上面代码的 C 层方法 nativeMethod() 中会回调 Java 层方法 callback()callback(String message)callbackAverage(int, int)callbackStatic()。下面我们看下 C 层是怎么实现的。

    C 层代码实现
    #include <jni.h>
    #include <stdio.h>
    #include "TestJNICallBackMethod.h"
    
    JNIEXPORT void JNICALL Java_TestJNICallBackMethod_nativeMethod(JNIEnv *env, jobject thisObj) {
        jclass thisClass = (*env)->GetObjectClass(env, thisObj);
    
        // 获取 method id,需要提供方法签名
        jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
        if (NULL == midCallBack) return;
        printf("In C, call back Java's callback()\n");
        // 根据 method id 回调 java 层方法
        (*env)->CallVoidMethod(env, thisObj, midCallBack);
    
        // 获取 method id,需要提供方法签名
        jmethodID midCallBackStr = (*env)->GetMethodID(env, thisClass, "callback", "(Ljava/lang/String;)V");
        if (NULL == midCallBackStr) return;
        printf("In C, call back Java's called(String)\n");
        jstring message = (*env)->NewStringUTF(env, "Hello from C");
        // 反射调用 java 层方法,提供 JNI 数据类型
        (*env)->CallVoidMethod(env, thisObj, midCallBackStr, message);
    
        jmethodID midCallBackAverage = (*env)->GetMethodID(env, thisClass, "callbackAverage", "(II)D");
        if (NULL == midCallBackAverage) return;
        jdouble average = (*env)->CallDoubleMethod(env, thisObj, midCallBackAverage, 2, 3);
        printf("In C, the average is %f\n", average);
    
        // 反射调用 static 方法并没有什么特殊的地方,只是 JNI 方法名不同而已
        jmethodID midCallBackStatic = (*env)->GetStaticMethodID(env, thisClass, "callbackStatic", "()Ljava/lang/String;");
        if (NULL == midCallBackStatic) return;
        jstring resultJNIStr = (*env)->CallStaticObjectMethod(env, thisClass, midCallBackStatic);
        const char *resultCStr = (*env)->GetStringUTFChars(env, resultJNIStr, NULL);
        if (NULL == resultCStr) return;
        printf("In C, the returned string is %s\n", resultCStr);
        // 通过 GetStringUTFChars 获得的资源,一定要用 ReleaseStringUTFChars 回收
        (*env)->ReleaseStringUTFChars(env, resultJNIStr, resultCStr);
    }
    

    上面的调用跟访问类属性时差不多,获取 Method ID 变成了 GetMethodID(),需要提供方法签名和方法名。
    回调方法通过传入 Method ID 调用相应的 Call<Primitive-type>Method()CallVoidMethod() 或者 CallObjectMethod() 方法。

    在 Native 层创建新的 Java 对象

    public class TestJNIConstructor {
        static {
           System.loadLibrary("myjni");
        }
      
        // Native method that calls back the constructor and return the constructed object.
        // Return an Integer object with the given int.
        private native Integer getIntegerObject(int number);
      
        public static void main(String args[]) {
           TestJNIConstructor obj = new TestJNIConstructor();
           System.out.println("In Java, the number is :" + obj.getIntegerObject(9999));
        }
     }
    

    上面代码将在 Native 层创建 Java 的实例对象,并返回给 Java 层。我们看看 C 层代码。

    C 层 实现创建 Java 层对象
    #include <jni.h>
    #include <stdio.h>
    #include "TestJNIConstructor.h"
    
    JNIEXPORT jobject JNICALL Java_TestJNIConstructor_getIntegerObject(JNIEnv *env, jobject thisObj, jint number) {
        // 先通过 findClass 找到我们要构建的对象的 Class
        jclass cls = (*env)->FindClass(env, "java/lang/Integer");
    
        // 获取我们要构建对象的构造函数
        jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "(I)V");
        if (NULL == midInit) return NULL;
        // 调用构造函数获取对象实例
        jobject newObj = (*env)->NewObject(env, cls, midInit, number);
    
        // 调用我们新构造出来的对象
        jmethodID midToString = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;");
        if (NULL == midToString) return NULL;
        jstring resultStr = (*env)->CallObjectMethod(env, newObj, midToString);
        const char *resultCStr = (*env)->GetStringUTFChars(env, resultStr, NULL);
        printf("In C: the number is %s\n", resultCStr);
    
        return newObj;
    }
    

    从上面代码可以看出,在 C 层创建 Java 对象跟我们在 C 层调用函数是差不多的,只不过我们调用的是 Java 层类的构造函数,然后通过 NewObject() 这样的方法构造出实例对象。

    还有其他类型的创建对象的方法:jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);jobject AllocObject(JNIEnv *env, jclass cls);。视当时的条件来调用不同的方法。

    对象数组的转换

    import java.util.ArrayList;
     
    public class TestJNIObjectArray {
       static {
          System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
       }
       // Native method that receives an Integer[] and
       //  returns a Double[2] with [0] as sum and [1] as average
       private native Double[] sumAndAverage(Integer[] numbers);
     
       public static void main(String args[]) {
          Integer[] numbers = {11, 22, 32};  // auto-box
          Double[] results = new TestJNIObjectArray().sumAndAverage(numbers);
          System.out.println("In Java, the sum is " + results[0]);  // auto-unbox
          System.out.println("In Java, the average is " + results[1]);
       }
    }
    

    我们传给 Native 层一个 Integer[],在 Native 层计算完成后,向 Java 层返回一个大小为 2 的 Double[] 数组,第一个为 Integer[] 数组的和,第二个为平均值。

    C 层代码实现
    #include <jni.h>
    #include <stdio.h>
    #include "TestJNIObjectArray.h"
    
    JNIEXPORT jobjectArray JNICALL Java_TestJNIObjectArray_sumAndAverage(JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) {
        // 由于对象数组中每个元素取出来都是 jobject,因此需要调用 Integer.intValue() 将其转换成 jint
        jclass classInteger = (*env)->FindClass(env, "java/lang/Integer");
        jmethodID midIntValue = (*env)->GetMethodID(env, classInteger, "intValue", "()I");
        if (NULL == midIntValue) return NULL;
    
        jsize length = (*env)->GetArrayLength(env, inJNIArray);
        jint sum = 0;
        int i;
        // 遍历对象数组的每个元素,将其求和
        for (i = 0; i < length; i++) {
            // 获取数组中的元素
            jobject objInteger = (*env)->GetObjectArrayElement(env, inJNIArray, i);
            if (NULL == objInteger) return NULL;
            jint value = (*env)->CallIntMethod(env, objInteger, midIntValue);
            sum += value;
        }
        double average = (double)sum / length;
        printf("In C, the sum is %d\n", sum);
        printf("In C, the average is %f\n", average);
    
        // 分配大小为 2 的 Double 数组
        jclass classDouble = (*env)->FindClass(env, "java/lang/Double");
        jobjectArray outJNIArray = (*env)->NewObjectArray(env, 2, classDouble, NULL);
    
        jmethodID midDoubleInit = (*env)->GetMethodID(env, classDouble, "<init>", "(D)V");
        if (NULL == midDoubleInit) return NULL;
        // 将结果创建两个 Double 对象
        jobject objSum = (*env)->NewObject(env, classDouble, midDoubleInit, (double)sum);
        jobject objAve = (*env)->NewObject(env, classDouble, midDoubleInit, average);
    
        // 将创建的两个 Double 对象放进 Double 数组
        (*env)->SetObjectArrayElement(env, outJNIArray, 0, objSum);
        (*env)->SetObjectArrayElement(env, outJNIArray, 1, objAve);
    
        return outJNIArray;
    }
    

    从上面我们可以注意到,对象数组有自己的一套 JNI 函数:NewObjectArray()GetObjectArrayElement()SetObjectArrayElement()

    本地和全局引用

    JNI 将对象的引用类型分为两类:本地引用全局引用

    1. 本地引用是由 native 方法创建的,当方法退出时,它会自动被回收。它在方法块的生命周期内都是合法的。可以显示调用 DeleteLocalRef() 方法以使 JVM 可以立即回收本地引用的资源。我们通过 native 方法传进来的 Java 对象都是本地引用,所有 JNI 方法返回的对象(jobject)也都是局部引用。

    2. 全局引用只有被程序显示调用 DeleteGlobalRef() 时,资源才会被回收。通过 NewGlobalRef() 来创建创建一个全局引用。

    public class TestJNIReference {
        static {
           System.loadLibrary("myjni");
        }
      
        // A native method that returns a java.lang.Integer with the given int.
        private native Integer getIntegerObject(int number);
      
        // Another native method that also returns a java.lang.Integer with the given int.
        private native Integer anotherGetIntegerObject(int number);
      
        public static void main(String args[]) {
           TestJNIReference test = new TestJNIReference();
           System.out.println(test.getIntegerObject(1));
           System.out.println(test.getIntegerObject(2));
           System.out.println(test.anotherGetIntegerObject(11));
           System.out.println(test.anotherGetIntegerObject(12));
           System.out.println(test.getIntegerObject(3));
           System.out.println(test.anotherGetIntegerObject(13));
        }
     }
    

    上面代码我们每一次调用 getIntegerObject() 或者 anotherGetIntegerObject() 时,C 层的代码都会希望保存上一次的 jclass 等信息。我们看看一种不规范的保存 JNI 资源的办法。

    不正确地保存 JNI 资源
    #include <jni.h>
    #include <stdio.h>
    #include "TestJNIReference.h"
    
    // 像传统 C 代码一样,使用全局静态变量。但是由于 JNI 函数返回的是 JNI 本地引用,
    // 因此不能简单地这样保存。在第二次访问 classInteger 时,就会报错,因为 classInteger 将会是一个非法值(但不是 NULL)
    static jclass classInteger;
    static jmethodID midIntegerInit;
    
    jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {
        if (NULL == classInteger) {
            printf("Find java.lang.Integer\n");
            classInteger = (*env)->FindClass(env, "java/lang/Integer");
        }
        if (NULL == classInteger) return NULL;
    
        if (NULL == midIntegerInit) {
            printf("Get Method ID for java.lang.Integer's constructor\n");
            midIntegerInit = (*env)->GetMethodID(env, classInteger, "<init>", "(I)V");
        }
        if (NULL == midIntegerInit) return NULL;
    
        jobject newObj = (*env)->NewObject(env, classInteger, midIntegerInit, number);
        printf("In C, constructed java.lang.Integer with number %d\n", number);
        return newObj;
    }
    
    JNIEXPORT jobject JNICALL Java_TestJNIReference_getIntegerObject
              (JNIEnv *env, jobject thisObj, jint number) {
       return getInteger(env, thisObj, number);
    }
     
    JNIEXPORT jobject JNICALL Java_TestJNIReference_anotherGetIntegerObject
              (JNIEnv *env, jobject thisObj, jint number) {
       return getInteger(env, thisObj, number);
    }
    

    上面的代码尝试重复使用 JNI 函数返回的本地引用,因此报错。我们如果想要重复使用 jclass,可以为其创建一个全局引用将其保存:

    if (NULL == classInteger) {
        printf("Find java.lang.Integer\n");
        // classInteger = (*env)->FindClass(env, "java/lang/Integer");
        jclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");
        classInteger = (*env)->NewGlobalRef(env, classIntegerLocal);
        (*env)->DeleteLocalRef(env, classIntegerLocal);
    }
    

    这里只能为 jclass 创建全局引用,因为 jmethodIDjfieldID 都不是 jobject,所以不是引用类型。

    相关文章

      网友评论

          本文标题:JNI 基本入门

          本文链接:https://www.haomeiwen.com/subject/gdeyrktx.html