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.javastatic {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 voidprivate native void sayHello();// Test Driverpublic 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 resourcejdouble 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 classjclass 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 IDjint number = env->GetIntField(thisObj, fidNumber);cout << "In C++, the int is " << number << endl;// Change the variablenumber = 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 IDjstring message =  (jstring)env->GetObjectField(thisObj, fidMessage);// Create a C-String with JNI Stringconst 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 stringmessage = 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[]"
描述符 含义
I int
B byte
S short
J long
F float
D double
C char
Z boolean
[I int[]
Ljava/lang/String String
[Ljava/lang/String String[]
  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 IDenv->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 TestJNICallBackMethodCompiled from "TestJNICallBackMethod.java"
public class TestJNICallBackMethod {public TestJNICallBackMethod();descriptor: ()Vprivate native void nativeMethod();descriptor: ()Vprivate void callback();descriptor: ()Vprivate void callback(java.lang.String);descriptor: (Ljava/lang/String;)Vprivate double callbackAverage(int, int);descriptor: (II)Dprivate static java.lang.String callbackStatic();descriptor: ()Ljava/lang/String;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vstatic {};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 argumentjobject newObj = env->NewObject(cls, midInit, number);// Try running the toString() on this newly create objectjmethodID 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 invokejobject 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 averageprivate native Double[] sumAndAverage(Integer[] numbers);public static void main(String args[]) {Integer[] numbers = {11, 22, 32};  // auto-boxDouble[] results = new TestJNIObjectArray().sumAndAverage(numbers);System.out.println("In Java, the sum is " + results[0]);  // auto-unboxSystem.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 intjmethodID 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.Doublejclass classDouble = env->FindClass("java/lang/Double");// Allocate a jobjectArray of 2 java.lang.DoublejobjectArray outJNIArray = env->NewObjectArray(2, classDouble, NULL);// Construct 2 Double objects by calling the constructorjmethodID 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 jobjectArrayenv->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 missingif (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 missingif (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 argumentjobject 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 missingif (NULL == classInteger) {printf("Find java.lang.Integer\n");// FindClass returns a local referencejclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");// Create a global reference from the local referenceclassInteger = (*env)->NewGlobalRef(env, classIntegerLocal);// No longer need the local reference, free it!(*env)->DeleteLocalRef(env, classIntegerLocal);}

JNI 简明教程之手把手教你入门相关推荐

  1. 腾讯云服务器使用教程,手把手教你入门

    腾讯云服务器使用教程包括注册账号实名认证.选择云服务器CVM或轻量应用服务器CPU内存带宽和系统盘配置.安全设置和云服务器远程连接.安全组端口开通教程.云服务器环境部署以搭建网站为例手把手网站上线,云 ...

  2. C# SuperSocket 手把手教你入门 傻瓜教程---5(探索自定义AppServer、AppSession,Conmmand,用配置文件App.comfig启动服务器)

    C# SuperSocket 手把手教你入门 傻瓜教程系列教程 C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据) C# SuperSocket 手把手教你 ...

  3. C# SuperSocket 手把手教你入门 傻瓜教程---3(Telnet服务器和客户端请求处理)

    C# SuperSocket 手把手教你入门 傻瓜教程系列教程 C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据) C# SuperSocket 手把手教你 ...

  4. C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据)

    C# SuperSocket 手把手教你入门 傻瓜教程系列教程 C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据) C# SuperSocket 手把手教你 ...

  5. Vue3 Typescript + Axios 全栈开发教程:手把手教你写「待办清单」APP

    本文完整版:<Vue3 Typescript + Axios 全栈开发教程:手把手教你写「待办清单」APP> Vue3 Typescript + Axios 全栈开发教程 前端 Vue3 ...

  6. Python学习教程:手把手教你搭建自己的量化分析数据库

    Python学习教程:手把手教你搭建自己的量化分析数据库 引言: 数据是金融量化分析的重要基础,包括股票历史交易数据.上市公司基本面数据.宏观和行业数据等.随着信息流量的日益膨胀,学会获取.查询和加工 ...

  7. 简单有趣的 NLP 教程:手把手教你用 PyTorch 辨别自然语言(附代码)

     简单有趣的 NLP 教程:手把手教你用 PyTorch 辨别自然语言(附代码) 雷锋网(公众号:雷锋网)按:本文作者甄冉冉,原载于作者个人博客,雷锋网已获授权. 最近在学pyTorch的实际应用 ...

  8. 手把手教你入门Git --- Git使用指南(Linux)

    手把手教你入门Git - Git使用指南(Linux) 系统:ubuntu 18.04 LTS 本文所有git命令操作实验具有连续性,git小白完全可以从头到尾跟着本文所有给出的命令走一遍,就会对gi ...

  9. python新手入门代码-新手必看:手把手教你入门 Python

    原标题:新手必看:手把手教你入门 Python 本文为 AI 研习社编译的技术博客,原标题 : Learning Python: From Zero to Hero 翻译 |永恒如新的日常校对 | 酱 ...

最新文章

  1. vue从入门到进阶:指令与事件(二)
  2. js轮播图代码_javascript基础(一)——轮播图
  3. 入门级实操教程!从概念到部署,全方位了解K8S Ingress!
  4. 带有AngularJS资源的Spring Rest Controller
  5. 【开源项目】Android下自定义HASH【支持一个key对应多个value--根据key排序】
  6. B端——复杂业务表单设计
  7. [paper reading] 译 + 注 :如何阅读 Research Papers(Andrew Ng)
  8. 【图像隐写】基于matlab高斯模型JPEG图像隐写【含Matlab源码 367期】
  9. 阿里云CDN工作原理、使用场景及产品优势简介
  10. [Gym] - 100886K 2015-2016 Petrozavodsk Winter Training Camp, Saratov SU Contest K - Toll Roads
  11. 读v_JULY_v整理笔试题博客有感,整理些答案。
  12. 雨课堂和微助教比较分析
  13. android高仿ios11系统,仿ios11状态栏安卓版-2020最新安卓仿iOS11状态栏apk3.0免费版下载_飞翔下载...
  14. openjdk windows版本下载地址
  15. java版本与javac版本不一致引起终端无法运行java文件问题
  16. JVM虚拟机概述(2)
  17. JAVA中的进制以及转换
  18. 【计算广告】移动设备 效果类广告 归因方式
  19. 学计算机语言的最佳年龄,孩子学编程最佳年龄是几岁
  20. 江小白新十年之战,迎来“国家队”入局助阵

热门文章

  1. java t9 字母组合_太赞了!美团T9终于整理出Java架构之完美设计实战开源文档
  2. swt 键盘事件ctrl+c_VB键盘事件详解
  3. epoch训练时间不同_神经网络训练的三个基本概念Epoch, Batch, Iteration
  4. jxls向右循环为什么会间隔单元格_VBA中单元格的Offset属性,你是否能灵活的利用呢?...
  5. 动态规划经典题目_动态规划经典题目:鸡蛋掉落(附视频讲解)
  6. arcmap导出地图快捷键_谷歌点坐标导出为excel表格
  7. 服务器能进系统滴滴响,ibm x226服务器开机嘀嘀嘀响三遍就没反应了。是怎么回事?内存吗?...
  8. php文件上传接口测试,七牛云存储-用php上传图片,我在本地测试,用php接口,不成功...
  9. java 删除已画出的线_如何删除java中的绘制线?
  10. 使用Arrays sort 方法進行排序