JNI 简明教程之手把手教你入门

1. 序言

有些时候,我们不可避免地要在 Java 中使用 native 代码(例如 C/C++),或是为了运行效率,或是为了不重复造轮子直接使用已有的 C/C++ 类库,或是为了防止被反编译。

JNI是一个比较复杂的技术,因为它涉及到了两种语言。

在这个教程开始之前,我假设你熟悉以下几个技术:

  • Java
  • C/C++ 和 gcc 编译

2. 开始JNI之旅

2.1 JNI与C

Step 1: 写一个HelloJNI.java去调用C代码
public class HelloJNI {  // Save as HelloJNI.java
   static {
      System.loadLibrary("hello"); // Load native library hello.dll (Windows) or libhello.so (Unixes)
                                   //  at runtime
                                   // This library contains a native method called sayHello()
   }
 
   // Declare an instance native method sayHello() which receives no parameter and returns void
   private native void sayHello();
 
   // Test Driver
   public static void main(String[] args) {
      new HelloJNI().sayHello();  // Create an instance and invoke the native method
   }
}

静态初始化中调用了System.loadLibrary去导入 “hello” 库(里面包含sayHello的native实现)。这个库应该被包含在 Java 库的搜索路径中,可以通过 -Djava.library.path=/path/to/lib 将其加入至搜索路径中。如果路径下没有找到要导入的库,会抛出一个 UnsatisfiedLinkError 错误。

然后,我们声明了一个叫 sayHello 的 native 方法,通过关键字 native 来表明这个方法的实现在另一种语言中。native 方法没有函数体,所以它的具体实现应该在导入的 hello 库中。

step 2: 编译Java程序 & 生成C/C++头文件 HelloJNI.h

JDK8 开始,你可以用 javac -h 来编译 java 程序,并且同时生成 C/C++ 头文件

> javac -h . HelloJNI.java

-h dir可以指定 C/C++ 头文件的存放路径(上面的例子中,.表示当前路径)

JDK8 之前,你需要先用 javac 编译 java 程序,然后用 javah 生成 C/C++ 头文件,如下例子。javahJDK10 开始被弃用。

> javac HelloJNI.java
> javah HelloJNI

让我们来看看生成的头文件 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

这个头文件声明了一个 Java_HelloJNI_sayHello 的C函数:

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

从 java 中 native 方法到 C 函数,函数命名的转换规则是:Java_{package_and_classname}_{function_name}(JNI_arguments)。Java包名中的".“将被转换为”_"

有两个参数:

  • JNIEnv*: 指向JNI环境,可以让你获取所有JNI函数
  • jobject: 指向"this"的Java对象
    在这个 hello-world 示例中不需要用到这两个参数,但是后面会用到他们。另外,我们先忽略 JNIEXPORTJNICALL 这个两个宏。

extern "C"告诉 C++ 编译器以 C 的方式来编译这个函数。C 和 C++ 有着不同的命名协议,因为 C++ 支持函数重载,用了不同的命名协议来处理重载的函数。

Step 3: 在 HelloJNI.c 中实现 C 程序
#include "HelloJNI.h"
#include <stdio.h>
#include <jni.h>

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj)
{
    printf("hello JNI");
    return;
}

jni.h由 JDK 提供,在 <JAVA_HOME>/include<JAVA_HOME>/include/win32(windows环境下)或者<JAVA_HOME>/include/linux(Linux环境下)或者<JAVA_HOME>/include/darwin(Mac os环境下)

关于JAVA_HOME的设置,

Step 4: 编译 HelloJNI.c

在Mac环境下编译分三步走:

  1. 设置 JAVA_HOME,在Mac下可以简单用以下命令来设置:
$ export JAVA_HOME=$(/usr/libexec/java_home)
  1. gccHelloJNI.c 编译成动态库
 $ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.c
  1. 运行 Java 程序
$ java -Djava.library.path=. HelloJNI
Step 4: 运行 Java 程序
> java HelloJNI

有时候你需要显示指明 native 库的具体存放位置,例如

> java -Djava.library.path=. HelloJNI

2.2 JNI与C++

我们还可以用 C++ 来实现

//
// Created by hw on 2019-07-20.
//

// Save as "HelloJNI.cpp"
#include <jni.h>       // JNI header provided by JDK
#include <iostream>    // C++ standard IO header
#include "HelloJNI.h"  // Generated
using namespace std;

// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj)
{
    cout << "Hello World from C++!" << endl;
    return;
}

编译的步骤与编译C程序类似,在 Mac OS 中可以使用如下命令编译:

g++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.cpp

然后运行:

> java -Djava.library.path=. HelloJNICpp

2.3 JNI与包

Java类通常属于某个包,这种情况下,JNI的编译和运行步骤为:

Step 1: JNI 代码 - myjni/HelloJNI.java
package myjni;

public class HelloJNI{
    static{
        System.loadLibrary("hello");
    }

    private native void sayHello();

    public static void main(String[] args){
        new HelloJNI().sayHello();
    }
}

Java类在 “myjni” 包中,并且保存在 “myjni/HelloJNI.java” 中

Step 2: 编译 JNI 程序 & 生成头文件
> javac -h include myjni\HelloJNI

在这个例子中,我们指定了头文件的存放路径。生成的文件为 include/myjni_HelloJNI.h

头文件中,native 函数的声明为:

JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *, jobject);
Step 3: 实现C/C++代码
// Save as "HelloJNI.cpp"
#include <jni.h>       // JNI header provided by JDK
#include <iostream>    // C++ standard IO header
#include "include/myjni_HelloJNI.h"  // Generated
using namespace std;

// Implementation of the native method sayHello()
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *env, jobject thisObj)
{
    cout << "Hello World from C++!" << endl;
    return;
}

编译:

g++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.cpp

运行:

java -Djava.library.path=. myjni.HelloJNI

3. JNI 基础知识

JNI中定义了与 Java 类型对应的 JNI类型:

  1. 原始类型:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean对应Java中的原始类型int, byte, short, long, float, double, char, boolean
  2. 引用类型:jobject对应java.lang.Object。还定义了以下的子类:
    • jclass对应java.lang.Class
    • jstring对应java.lang.String
    • jthrowable对应java.lang.Throwable
    • jarray对应 Java 的数组类型,有八种原始类型的数组,jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray;和一种 object 数组 jobjectArray

native 函数通过JNI类型将Java层的参数传入(例如jstring),然后 native 函数将JNI类型转换为可处理的类型(例如将jstring转换为char*),因此需要一个将JNI类型转换到native类型的过程。

native程序主要做了这么几件事:

  1. 接收 JNI 类型的参数
  2. 对于引用类型的JNI参数,需要转换为native类型,例如jstring转换到char*jintArrayint[]。原始类型的参数,例如 jintjdouble不用转换,直接可用。
  3. 执行native代码
  4. 创建一个JNI类型的返回对象,将结果拷贝到这个对象
  5. 返回结果

JNI编程中,最令人困惑和具有挑战性就是JNI类型和native类型之间的相互转换。JNI环境中提供了很多有用的函数来帮助转换。

4. 在Java和Native程序中传递参数和结果

4.1 传递原始参数类型

传递原始类型的数据简单又直接,jxxx类型的JNI类型,例如jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean对应Java原始类型int, byte, short, long, float, double, char and boolean

Java JNI 程序:TestJNIPrimitive.java
public class TestJNIPrimitive{
    static{
        System.loadLibrary("myjni");
    }

    private native double average(int n1, int n2);

    public static void main(String[] args){
        System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
    }
}

native方法double average(int n1, int n2)接收两个int参数,然后返回一个double类型的结果。

首先对Java程序进行编译,同时生成头文件

javac -h . TestJNIPrimitive.java

然后在 TestJNIPrimitive.cpp 中实现native方法

#include "TestJNIPrimitive.h"
#include <iostream>

using namespace std;

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(
    JNIEnv *env, jobject obj, jint n1, jint n2)
{
    cout << "n1 = " << n1 << ", n2 = " << n2 << endl;

    return jdouble(n1 + n2)/2.0;
}

可以在 jni.hdarwin/jni_md.h中找到一堆的typedef语句定义了JNI的原始类型,加上一个jsize

// In "darwin/jni_md.h"
typedef int jint;
#ifdef _LP64
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;

// In "jni.h"
typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;

typedef jint            jsize;

对程序进行编译:

g++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libmyjni.dylib TestJNIPrimitive.cpp

运行:

java -Djava.library.path=. TestJNIPrimitive
# n1 = 3, n2 = 2
# In Java, the average is 2.5

4.2 传递字符串

Java JNI 程序:TestJNIString.java
public class TestJNIString{
    static {
        System.loadLibrary("myjni");
    }

    private native String sayHello(String msg);

    public static void main(String[] args){
        String result = new TestJNIString().sayHello("Hello from Java");
        System.out.println(result);
    }
}

我们定义了一个sayHello的native方法,其参数是 String 类型,且返回值也是 String

编译,生成头文件

javac -h . TestJNIString.java

头文件中native方法的声明为:

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(JNIEnv *, jobject, jstring);

JNI中定义了jstring来对应 Java 中的 String 类型。在 native 方法中,Java 中的 String 类型以jstringJNI类型传入,返回值也是jstring类型。

在 JNI 和 Java 传递字符串的难度比传递原始类型多多了,因为 String 是一个对象,而 C 中字符是一个 char* 的原始类型,你需要在这两者中做一个转换。

JNI环境提供了这样的转换函数:

  1. const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)jstring转换为char*
  2. jstring NewStringUTF(JNIEnv*, char*)char*转换为jstring

具体的实现 TestJNIString.cpp

#include "TestJNIString.h"
#include <iostream>
using namespace std;

JNIEXPORT jstring JNICALL Java_TestJNIString_sayHello(
    JNIEnv *env, jobject obj, jstring inJNIString)
{
    const char* inStr = env->GetStringUTFChars(inJNIString, NULL);
    if(NULL == inStr)
        return NULL;

    cout << "the received string is " << inStr << endl;
    env->ReleaseStringUTFChars(inJNIString, inStr);

    string outString;
    cout << "Enter a String:";
    cin >> outString;

    return env->NewStringUTF(outString.c_str());
}
JNI 中的字符串函数

JNI支持Unicode(16位字符)和UTF-8(1-3字节编码)字符串的转换。UTF-8字符串的作用类似于以null结尾的C字符串(字符数组),应该在C / C ++程序中使用。

JNI中提供了以下关于字符串的函数:

// UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
// Can be mapped to null-terminated char-array C-string
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding.
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
   // Informs the VM that the native code no longer needs access to utf.
jstring NewStringUTF(JNIEnv *env, const char *bytes);
   // Constructs a new java.lang.String object from an array of characters in modified UTF-8 encoding.
jsize GetStringUTFLength(JNIEnv *env, jstring string);
   // Returns the length in bytes of the modified UTF-8 representation of a string.
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
   // Translates len number of Unicode characters beginning at offset start into modified UTF-8 encoding 
   // and place the result in the given buffer buf.
  
// Unicode Strings (16-bit character)
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
   // Returns a pointer to the array of Unicode characters
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
   // Informs the VM that the native code no longer needs access to chars.
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);
   // Constructs a new java.lang.String object from an array of Unicode characters.
jsize GetStringLength(JNIEnv *env, jstring string);
   // Returns the length (the count of Unicode characters) of a Java string.
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);
   // Copies len number of Unicode characters beginning at offset start to the given buffer buf
UTF-8 字符串

GetStringUTFChars函数可以从jstring类型中创建出char*类型的字符串,如果内存没有申请成功将会返回NULL。

GetStringUTFChars第三个参数isCopy(jboolean*类型),如果其值为 JNI_TRUR,那么返回一份java.lang.String实例的拷贝,如果其值位 JNI_FALSE,那么直接返回java.lang.String实例的指针(这种情况下,native代码如果修改了返回字符串的值,那么在 Java 层,其字符串的值也会发生变化)。

当不在需要GetStringUTFChars返回的字符串时,记得要用ReleaseStringUTFChars释放内存以及引用,以便让 Java 做垃圾回收。

NewStringUTF用一个 C 字符串创建 JNI 字符串

从 JDK1.2 开始引入了 GetStringUTFRegion,它可以复制部分(从 start 到 end)jstring 到 “预分配”的 C 字符串数组中。

Unicode 字符串

jchar* 而不是 char* 来存放 Unicode 字符串

4.3 传递原始数据数组

Java JNI 程序 - TestJNIPrimitiveArray.java
public class TestJNIPrimitiveArray{
    static {
        System.loadLibrary("myjni");
    }

    // 返回数组double[2],其中double[0]为和,double[1]为平均数
    private native double[] sumAndAverage(int[] numbers);

    public static void main(String[] args){
        int[] numbers = {22,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]);
    }
}

生成的头文件中包含了 native 方法的函数声明:

JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage(JNIEnv *, jobject, jintArray);

在 Java 中,数组属于引用类型。就如 3. JNI 基础知识 中提到的,一共有9中原始类型数组。

同样的,你需要将 JNI 类型转换为 native 类型,例如将 jintArray 转换为 jint[],或者将 jdoubleArray 转换为 jdouble[]。 JNI 环境提供了一些有用函数用来转换:

  1. jintArray 得到 jint[],只需要调用jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy) 即可
  2. jint[]jintArray,首先调用 jintArray NewIntArray(JNIEnv *env, jsize len) 申请内存,然后用 void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf) 复制 jint[] 的数据到 jintArray中。

在做原始数组传时,native 程序需要做:

  1. 接收 JNI 数组(例如 jintArray),将其转换为 native 数组(例如,jint[])
  2. 对 native 数组进行预想的操作
  3. 将 native 数组转换为 JNI 数组,返回至 Java

具体实现 TestJNIPrimitiveArray.cpp

#include "TestJNIPrimitiveArray.h"
#include <iostream>

using namespace std;

JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage(
    JNIEnv *env, jobject obj, jintArray inJNIArray)
{
    jint* inArray = env->GetIntArrayElements(inJNIArray, NULL);
    if(NULL == inArray) return NULL;
    jsize length = env->GetArrayLength(inJNIArray);

    jint sum = 0;
    for(int i = 0; i < length; ++i)
    {
        sum += inArray[i];
    }

    jdouble average = (jdouble)sum / length;
    env->ReleaseIntArrayElements(inJNIArray, inArray, 0); // release resource

    jdouble outArray[] = {sum, average};
    jdoubleArray outJNIArray = env->NewDoubleArray(2);
    if(NULL == outJNIArray) return NULL;
    env->SetDoubleArrayRegion(outJNIArray, 0, 2, outArray);
    return outJNIArray;

}

5 访问对象变量和回调方法

5.1 访问对象的实例变量

Java JNI 程序:
public class TestJNIInstanceVariable{
    static {
        System.loadLibrary("myjni");
    }

    private int number = 88;
    private String message = "Hello from Java";

    private native void modifyInstanceVariable();

    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);
    }
}

这个类有两个成员变量:numbermessage,分别是 intString 类型。 modifyInstanceVariable 方法将在 native 层修改这两个成员变量的值。

TestJNIInstanceVariable.c
#include "TestJNIInstanceVariable.h"
#include <iostream>

using namespace std;

JNIEXPORT void JNICALL Java_TestJNIInstanceVariable_modifyInstanceVariable(
    JNIEnv *env, jobject thisObj)
{
    // Get a reference to this object's class
    jclass thisClass = env->GetObjectClass(thisObj);

    // Int
    // Get the Field ID of the instance variables "number"
    jfieldID fidNumber = env->GetFieldID(thisClass, "number", "I");
    if(NULL == fidNumber) return;

    // Get the int given the Field ID
    jint number = env->GetIntField(thisObj, fidNumber);
    cout << "In C++, the int is " << number << endl;

    // Change the variable
    number = 99;
    env->SetIntField(thisObj, fidNumber, number);

    // String
    // Get the Field ID of the instance variables "message"
    jfieldID fidMessage = env->GetFieldID(thisClass, "message", "Ljava/lang/String;");
    if(NULL == fidMessage) return;

    // Get the int given the Field ID
    jstring message =  (jstring)env->GetObjectField(thisObj, fidMessage);

    // Create a C-String with JNI String
    const char* str = env->GetStringUTFChars(message, NULL);
    if(NULL == str) return;

    cout << "In C++, the string is " << str << endl;

    // Create a new C-String and assign to the JNI string
    message = env->NewStringUTF("Hello from C++");
    if(NULL == message) return;

    env->SetObjectField(thisObj, fidMessage, message);
}

为了访问实例的成员变量,你需要:

  1. 通过GetObjectClass()获取对该对象的类的引用。
  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[]"
描述符含义
Iint
Bbyte
Sshort
Jlong
Ffloat
Ddouble
Cchar
Zboolean
[Iint[]
Ljava/lang/StringString
[Ljava/lang/StringString[]
  1. 基于字段 ID,通过 GetObjectField() 或者 Get<primitive-type>Field() 获取实例变量
  2. 通过 SetObjectField() 或者 Set<primitive-type>Field() 更新实例变量

5.2 访问类的静态变量

访问静态变量的流程和访问实例变量类型,只不过方法换成了 GetStaticFieldID(), Get|SetStaticObjectField(), Get|SetStatic<Primitive-type>Field()

Java JNI 程序:TestJNIStaticVariable.java
public class TestJNIStaticVariable{
    static {
        System.loadLibrary("myjni");
    }

    private static double number = 55.66;

    private native void modifyStaticVariable();

    public static void main(String args[]) {
        TestJNIStaticVariable test = new TestJNIStaticVariable();
        test.modifyStaticVariable();
        System.out.println("In Java, the double is " + number);
    }
}
TestJNIStaticVariable.cpp
#include "TestJNIStaticVariable.h"
#include <iostream>
using namespace std;

JNIEXPORT void JNICALL Java_TestJNIStaticVariable_modifyStaticVariable(
    JNIEnv *env, jobject thisObj)
{
    jclass thisClass = env->GetObjectClass(thisObj);

    jfieldID fidNumber = env->GetStaticFieldID(thisClass, "number", "D");
    if(NULL == fidNumber) return;

    jdouble number = env->GetStaticDoubleField(thisClass, fidNumber);
    cout << "In C++, the double is " << number << endl;

    number = 77.88;
    env->SetStaticDoubleField(thisClass, fidNumber, number);
}

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

我们还可在 native 层调用实例的方法

Java JNI 程序:TestJNICallBackMethod.java
public class TestJNICallBackMethod{
    static {
        System.loadLibrary("myjni");
    }

    private native void nativeMethod();

    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;
    }

    private static String callbackStatic(){
        return "From static Java method";
    }

    public static void main(String[] args){
        new TestJNICallBackMethod().nativeMethod();
    }
}
TestJNICallBackMethod.cpp
#include "TestJNICallBackMethod.h"
#include <iostream>

using namespace std;

JNIEXPORT void JNICALL Java_TestJNICallBackMethod_nativeMethod(
    JNIEnv *env, jobject thisObj)
{
    jclass thisClass = env->GetObjectClass(thisObj);

    jmethodID midCallBack = env->GetMethodID(thisClass, "callback", "()V");
    if(NULL == midCallBack) return;
    cout << "In C++, call back Java's callback()\n";

    // Call back the method (which returns void), based on the Method ID
    env->CallVoidMethod(thisObj, midCallBack);

    jmethodID midCallBackStr = env->GetMethodID(thisClass, "callback", "(Ljava/lang/String;)V");
    if(NULL == midCallBackStr) return;
    cout << "In C++, call back Java's callback(String)\n";
    jstring message = env->NewStringUTF("Hello from C++");
    env->CallVoidMethod(thisObj, midCallBackStr, message);

    jmethodID midCallBackAverage = env->GetMethodID(thisClass, "callbackAverage", "(II)D");
    if(NULL == midCallBackAverage) return;
    jdouble average = env->CallDoubleMethod(thisObj, midCallBackAverage, 2, 3);
    cout << "In C++, the average is " << average << endl;

    jmethodID midCallBackStatic = env->GetStaticMethodID(thisClass, "callbackStatic", "()Ljava/lang/String;");
    if(NULL == midCallBackStatic) return;
    jstring resultJNIStr = (jstring)env->CallStaticObjectMethod(thisClass, midCallBackStatic);
    const char* resultStr = env->GetStringUTFChars(resultJNIStr, NULL);
    if(NULL == resultStr) return;
    cout << "In C++, the returned string is " << resultStr << endl;
    env->ReleaseStringUTFChars(resultJNIStr, resultStr);
}

为了回调实例中的方法,你需要:

  1. 通过GetObjectClass()获取对该对象的类的引用。
  2. 通过 GetMethodID() 获取方法的 ID,你需要提供方法的描述符,方法描述符的形式为”(parameters)return-type“。你可以通过 javap 工具加上 -s(打印描述符)和-p(显示私有变量)来查询方法的描述符
>javap -s -p TestJNICallBackMethod

Compiled from "TestJNICallBackMethod.java"
public class TestJNICallBackMethod {
  public TestJNICallBackMethod();
    descriptor: ()V

  private native void nativeMethod();
    descriptor: ()V

  private void callback();
    descriptor: ()V

  private void callback(java.lang.String);
    descriptor: (Ljava/lang/String;)V

  private double callbackAverage(int, int);
    descriptor: (II)D

  private static java.lang.String callbackStatic();
    descriptor: ()Ljava/lang/String;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V

  static {};
    descriptor: ()V
}
  1. 基于方法 ID,你可以调用 Call<Primitive-type>Method() or CallVoidMethod() or CallObjectMethod() 方法,并且将方法的参数传递进去。

另外,你需要GetStaticMethodID(), CallStatic<Primitive-type>Method(), CallStaticVoidMethod() or CallStaticObjectMethod(). 来调用静态方法。

JNI中用于回调的函数如下:

jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.
   
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.

5.4 回调 super.xx() 方法

JNI 提供了一组 CallNonvirtual<Type>Method() 函数来调用在子类重写的方法(类型在 Java 中调用 super.methodName())

  1. 通过 GetMethodID() 获取方法ID
  2. 基于方法ID,调用 CallNonvirtual<Type>Method() 来回调父类方法

JNI 中用于调用重载的父类方法函数有:

NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);

6. 创建对象和对象数组

我们可以在 native 层通过调用 NewObject()newObjectArray() 来创建对象或者对象数组,并返回至 Java 层。

6.1 在 native 中创建对象

创建对象时,我们首先用GetMethodID获取到对象的构造函数(构造函数的名字是""),然后用NewObject()去调用构造函数来创建对象

Java JNI 程序:TestJavaConstructor.java
public class TestJavaConstructor{
    static {
        System.loadLibrary("myjni");
    }

    private native Integer getIntegerObject(int number);

    public static void main(String[] args){
        TestJavaConstructor obj = new TestJavaConstructor();
        System.out.println("In Java, the number is : " + obj.getIntegerObject(9999));
    }
}
TestJavaConstructor.cpp
#include "TestJavaConstructor.h"
#include <iostream>
using namespace std;

JNIEXPORT jobject JNICALL Java_TestJavaConstructor_getIntegerObject
    (JNIEnv *env, jobject thisObj, jint number)
{
    jclass cls = env->FindClass("java/lang/Integer");

    jmethodID midInit = env->GetMethodID(cls, "<init>", "(I)V");
    if(NULL == midInit) return NULL;

    // Call back constructor to allocate a new instance, with an int argument
    jobject newObj = env->NewObject(cls, midInit, number);

    // Try running the toString() on this newly create object
    jmethodID midToString = env->GetMethodID(cls, "toString", "()Ljava/lang/String;");
    if (NULL == midToString) return NULL;

    jstring resultJNIStr = (jstring)env->CallObjectMethod(newObj, midToString);
    const char *resultStr = env->GetStringUTFChars(resultJNIStr, NULL);
    cout << "In C++, the number is " << resultStr << endl;

    return newObj;
}

JNI 中用于创建对象的函数有:

jclass FindClass(JNIEnv *env, const char *name);
 
jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
   // Constructs a new Java object. The method ID indicates which constructor method to invoke
 
jobject AllocObject(JNIEnv *env, jclass cls);
  // Allocates a new Java object without invoking any of the constructors for the object.

6.2 对象数组

Java JNI 程序:TestJNIObjectArray.java
import java.util.ArrayList;
 
public class TestJNIObjectArray {
   static {
      System.loadLibrary("myjni"); 
   }
   // 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]);
   }
}
TestJNIObjectArray.cpp
#include "TestJNIObjectArray.h"
#include <iostream>
using namespace std;

JNIEXPORT jobjectArray JNICALL Java_TestJNIObjectArray_sumAndAverage(
    JNIEnv *env, jobject thisObj, jobjectArray inJNIArray)
{
    jclass classInteger = env->FindClass("java/lang/Integer");

    // Use Integer.intValue() to retrieve the int
    jmethodID midIntValue = env->GetMethodID(classInteger, "intValue", "()I");
    if (NULL == midIntValue) return NULL;

    jsize length = env->GetArrayLength(inJNIArray);
    jint sum = 0;
    for (int i = 0; i < length; i++) {
        jobject objInteger = env->GetObjectArrayElement(inJNIArray, i);
        if (NULL == objInteger) return NULL;
        jint value = env->CallIntMethod(objInteger, midIntValue);
        sum += value;
    }

    double average = (double)sum / length;
    cout << "In C++, the sum is " << sum << endl;
    cout << "In C++, the average is " << average << endl;

    // Get a class reference for java.lang.Double
    jclass classDouble = env->FindClass("java/lang/Double");

    // Allocate a jobjectArray of 2 java.lang.Double
    jobjectArray outJNIArray = env->NewObjectArray(2, classDouble, NULL);

    // Construct 2 Double objects by calling the constructor
    jmethodID midDoubleInit = env->GetMethodID(classDouble, "<init>", "(D)V");
    if (NULL == midDoubleInit) return NULL;
    jobject objSum = env->NewObject(classDouble, midDoubleInit, (double)sum);
    jobject objAve = env->NewObject(classDouble, midDoubleInit, average);

    // Set to the jobjectArray
    env->SetObjectArrayElement(outJNIArray, 0, objSum);
    env->SetObjectArrayElement(outJNIArray, 1, objAve);

    return outJNIArray;
}

JNI 中用于创建和操作对象数组的函数有:

jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
   // Constructs a new array holding objects in class elementClass.
   // All elements are initially set to initialElement.
 
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
   // Returns an element of an Object array.
 
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
   // Sets an element of an Object array.

7. 局部和全局引用

在 native 方法中,我们经常使用 FindClass(), GetMethodID(), GetFieldID() 来获取 jclass, jmethodIDjfieldID。这些方法的调用成本很高,我们应该获取一次并且将其缓存以供后续使用,而不是重复执行调用,从而消除开销。

JNI 中 native 代码使用的对象引用分为两种:局部引用和全局引用:

  1. 局部引用在 native 方法中创建,并在退出 native 方法时销毁。可以使用 DeleteLocalRef() 显示的使局部引用失效,以便可以进行垃圾回收。
  2. 全局引用只有显示使用 DeleteGlobalRef() 才会被销毁。可以通过 NewGlobalRef() 从局部引用创建全局引用。
举个例子
public class TestJNIReference {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }
 
   // 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));
   }
}

上面的代码有两个 native 方法,它们都返回 java.lang.Integer 对象。

在 C/C++ 代码中,我们通过 FindClass() 需要获取 java.lang.Integer 的引用。然后找到 Integer 的构造函数ID。我们希望将这些都缓存起来以消除开销。

下面代码是不起作用的:

#include <jni.h>
#include <stdio.h>
#include "TestJNIReference.h"
 
// Global Reference to the Java class "java.lang.Integer"
static jclass classInteger;
static jmethodID midIntegerInit;
 
jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {
 
   // Get a class reference for java.lang.Integer if missing
   if (NULL == classInteger) {
      printf("Find java.lang.Integer\n");
      classInteger = (*env)->FindClass(env, "java/lang/Integer");
   }
   if (NULL == classInteger) return NULL;
 
   // Get the Method ID of the Integer's constructor if missing
   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;
 
   // Call back constructor to allocate a new instance, with an int argument
   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);
}

上述代码中,我们调用 FindClass() 来获取 java.lang.Integer 的引用,并保存在全局的静态变量中。尽管如此,在下一次调用中,此引用不再有效(并且不是NULL)。这是因为FindClass()返回一个本地引用,一旦该方法退出就会失效。

为了解决这个问题,我们需要从FindClass()返回的局部引用创建一个全局引用。然后我们可以释放局部引用。修改后的代码如下:

   // Get a class reference for java.lang.Integer if missing
   if (NULL == classInteger) {
      printf("Find java.lang.Integer\n");
      // FindClass returns a local reference
      jclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");
      // Create a global reference from the local reference
      classInteger = (*env)->NewGlobalRef(env, classIntegerLocal);
      // No longer need the local reference, free it!
      (*env)->DeleteLocalRef(env, classIntegerLocal);
   }
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值