JNI 基本入门

使用 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 
/* 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 header provided by JDK
#include       // 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 
#include 
#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 
#include 
#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 
#include 
#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 
#include 
#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;",需要将全限定名中的 '.' 换成 '/',如 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() 或者 GetField() 方法获取该属性在当前实例中的值。
  2. 通过属性 id,调用 SetObjectField() 或者 SetField() 方法重新设置该属性在当前实例中的值。

访问类静态变量

方法跟上一节中 访问实例变量 差不多,只不过访问方法变成了 GetStaticFieldID()Get|SetStaticObjectField()Get|SetStaticField()

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

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 
#include 
#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 调用相应的 CallMethod()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 
#include 
#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, "", "(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 
#include 
#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, "", "(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 
#include 
#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, "", "(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 基本入门)