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.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++ 头文件,如下例子。javah
从 JDK10 开始被弃用。
> 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 示例中不需要用到这两个参数,但是后面会用到他们。另外,我们先忽略JNIEXPORT
和JNICALL
这个两个宏。
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环境下编译分三步走:
- 设置
JAVA_HOME
,在Mac下可以简单用以下命令来设置:
$ export JAVA_HOME=$(/usr/libexec/java_home)
- 用
gcc
将HelloJNI.c
编译成动态库
$ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib -o libhello.dylib HelloJNI.c
- 运行 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类型:
- 原始类型:
jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean
对应Java中的原始类型int, byte, short, long, float, double, char, boolean
- 引用类型:
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程序主要做了这么几件事:
- 接收 JNI 类型的参数
- 对于引用类型的JNI参数,需要转换为native类型,例如
jstring
转换到char*
,jintArray
到int[]
。原始类型的参数,例如jint
和jdouble
不用转换,直接可用。 - 执行native代码
- 创建一个JNI类型的返回对象,将结果拷贝到这个对象
- 返回结果
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.h
和 darwin/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 类型以jstring
JNI类型传入,返回值也是jstring
类型。
在 JNI 和 Java 传递字符串的难度比传递原始类型多多了,因为 String 是一个对象,而 C 中字符是一个 char*
的原始类型,你需要在这两者中做一个转换。
JNI环境提供了这样的转换函数:
const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)
将jstring
转换为char*
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 环境提供了一些有用函数用来转换:
- 从
jintArray
得到jint[]
,只需要调用jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)
即可 - 从
jint[]
到jintArray
,首先调用jintArray NewIntArray(JNIEnv *env, jsize len)
申请内存,然后用void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)
复制jint[]
的数据到jintArray
中。
在做原始数组传时,native 程序需要做:
- 接收 JNI 数组(例如 jintArray),将其转换为 native 数组(例如,jint[])
- 对 native 数组进行预想的操作
- 将 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);}
}
这个类有两个成员变量:number和message,分别是 int
和 String
类型。 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);
}
为了访问实例的成员变量,你需要:
- 通过GetObjectClass()获取对该对象的类的引用。
- 通过
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[] |
- 基于字段 ID,通过
GetObjectField()
或者Get<primitive-type>Field()
获取实例变量 - 通过
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);
}
为了回调实例中的方法,你需要:
- 通过GetObjectClass()获取对该对象的类的引用。
- 通过
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
}
- 基于方法 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()
)
- 通过
GetMethodID()
获取方法ID - 基于方法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, jmethodID
和 jfieldID
。这些方法的调用成本很高,我们应该获取一次并且将其缓存以供后续使用,而不是重复执行调用,从而消除开销。
JNI 中 native 代码使用的对象引用分为两种:局部引用和全局引用:
- 局部引用在 native 方法中创建,并在退出 native 方法时销毁。可以使用
DeleteLocalRef()
显示的使局部引用失效,以便可以进行垃圾回收。 - 全局引用只有显示使用
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 简明教程之手把手教你入门相关推荐
- 腾讯云服务器使用教程,手把手教你入门
腾讯云服务器使用教程包括注册账号实名认证.选择云服务器CVM或轻量应用服务器CPU内存带宽和系统盘配置.安全设置和云服务器远程连接.安全组端口开通教程.云服务器环境部署以搭建网站为例手把手网站上线,云 ...
- C# SuperSocket 手把手教你入门 傻瓜教程---5(探索自定义AppServer、AppSession,Conmmand,用配置文件App.comfig启动服务器)
C# SuperSocket 手把手教你入门 傻瓜教程系列教程 C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据) C# SuperSocket 手把手教你 ...
- C# SuperSocket 手把手教你入门 傻瓜教程---3(Telnet服务器和客户端请求处理)
C# SuperSocket 手把手教你入门 傻瓜教程系列教程 C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据) C# SuperSocket 手把手教你 ...
- C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据)
C# SuperSocket 手把手教你入门 傻瓜教程系列教程 C# SuperSocket 手把手教你入门 傻瓜教程---1(服务器单向接收客户端发送数据) C# SuperSocket 手把手教你 ...
- Vue3 Typescript + Axios 全栈开发教程:手把手教你写「待办清单」APP
本文完整版:<Vue3 Typescript + Axios 全栈开发教程:手把手教你写「待办清单」APP> Vue3 Typescript + Axios 全栈开发教程 前端 Vue3 ...
- Python学习教程:手把手教你搭建自己的量化分析数据库
Python学习教程:手把手教你搭建自己的量化分析数据库 引言: 数据是金融量化分析的重要基础,包括股票历史交易数据.上市公司基本面数据.宏观和行业数据等.随着信息流量的日益膨胀,学会获取.查询和加工 ...
- 简单有趣的 NLP 教程:手把手教你用 PyTorch 辨别自然语言(附代码)
简单有趣的 NLP 教程:手把手教你用 PyTorch 辨别自然语言(附代码) 雷锋网(公众号:雷锋网)按:本文作者甄冉冉,原载于作者个人博客,雷锋网已获授权. 最近在学pyTorch的实际应用 ...
- 手把手教你入门Git --- Git使用指南(Linux)
手把手教你入门Git - Git使用指南(Linux) 系统:ubuntu 18.04 LTS 本文所有git命令操作实验具有连续性,git小白完全可以从头到尾跟着本文所有给出的命令走一遍,就会对gi ...
- python新手入门代码-新手必看:手把手教你入门 Python
原标题:新手必看:手把手教你入门 Python 本文为 AI 研习社编译的技术博客,原标题 : Learning Python: From Zero to Hero 翻译 |永恒如新的日常校对 | 酱 ...
最新文章
- vue从入门到进阶:指令与事件(二)
- js轮播图代码_javascript基础(一)——轮播图
- 入门级实操教程!从概念到部署,全方位了解K8S Ingress!
- 带有AngularJS资源的Spring Rest Controller
- 【开源项目】Android下自定义HASH【支持一个key对应多个value--根据key排序】
- B端——复杂业务表单设计
- [paper reading] 译 + 注 :如何阅读 Research Papers(Andrew Ng)
- 【图像隐写】基于matlab高斯模型JPEG图像隐写【含Matlab源码 367期】
- 阿里云CDN工作原理、使用场景及产品优势简介
- [Gym] - 100886K 2015-2016 Petrozavodsk Winter Training Camp, Saratov SU Contest K - Toll Roads
- 读v_JULY_v整理笔试题博客有感,整理些答案。
- 雨课堂和微助教比较分析
- android高仿ios11系统,仿ios11状态栏安卓版-2020最新安卓仿iOS11状态栏apk3.0免费版下载_飞翔下载...
- openjdk windows版本下载地址
- java版本与javac版本不一致引起终端无法运行java文件问题
- JVM虚拟机概述(2)
- JAVA中的进制以及转换
- 【计算广告】移动设备 效果类广告 归因方式
- 学计算机语言的最佳年龄,孩子学编程最佳年龄是几岁
- 江小白新十年之战,迎来“国家队”入局助阵
热门文章
- java t9 字母组合_太赞了!美团T9终于整理出Java架构之完美设计实战开源文档
- swt 键盘事件ctrl+c_VB键盘事件详解
- epoch训练时间不同_神经网络训练的三个基本概念Epoch, Batch, Iteration
- jxls向右循环为什么会间隔单元格_VBA中单元格的Offset属性,你是否能灵活的利用呢?...
- 动态规划经典题目_动态规划经典题目:鸡蛋掉落(附视频讲解)
- arcmap导出地图快捷键_谷歌点坐标导出为excel表格
- 服务器能进系统滴滴响,ibm x226服务器开机嘀嘀嘀响三遍就没反应了。是怎么回事?内存吗?...
- php文件上传接口测试,七牛云存储-用php上传图片,我在本地测试,用php接口,不成功...
- java 删除已画出的线_如何删除java中的绘制线?
- 使用Arrays sort 方法進行排序