概述
JNI 是 Java Native Interface 的简写,可以译作 Java 原生接口。
JNI 定义了两个关键数据结构,即 JavaVM
和 JNIEnv
, 两者本质上都是指向函数表的二级指针。
- JavaVM: Java 虚拟机在 JNI 层的代表,理论上每个进程可以有多个 JavaVM, 但在 Android 中一个进程只允许有一个 JavaVM。
- JNIENV: 代表了 Java 在本线程的运行环境,每个线程都有一个。无法在线程之间共享 JNIEnv, 如果一段代码无法通过其他方法获取自己的 JNIEnv,则应该共享相应的 JavaVM,然后使用 GetEnv 发现线程的 JNIEnv。
JNI 中所有线程都是 Linux 线程,由内核调度。线程通常从受管理代码启动(Thread.start()),也可以在其他位置创建,然后附加到 JavaVM。例如,可以使用 AttachCurrentThread() 或 AttachCurrentThreadAsDaemon() 函数附加通过 pthread_create() 或 std::thread 启动的线程。在附加之前,线程不包含任何 JNIEnv,也无法调用 JNI。通常,最好使用 Thread.start() 创建需要调用 Java 代码的任何线程。这样做可以确保您有足够的堆栈空间、属于正确的 ThreadGroup 且与您的 Java 代码使用相同的 ClassLoader。
附加原生创建的线程会构建 java.lang.Thread 对象并将其添加到“主”ThreadGroup,从而使调试程序能够看到它。在已附加的线程上调用 AttachCurrentThread() 属于空操作。通过 JNI 附加的线程在退出之前必须调用 DetachCurrentThread()。
引用
局部引用
通过 NewLocalRef 和各种 JNI 接口创建(FindClass, NewObject, GetObjectClass 和 NewCharArray 等)创建的引用,从 Native 方法返回到 Java 层之后,如果 Java 层没有对返回的局部引用使用的话,局部引用就会被JVM自动释放。释放一个局部引用有两种方式,一个是本地方法执行完毕后 JVM 自动释放,另外一个是自己调用 DeleteLocalRef 手动释放。
JNI 会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。所以在不需要局部引用时可以调用 DeleteLocalRef 手动删除。
全局引用
调用 NewGlobalRef 基于局部引用创建的引用,它会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。JVM 不会自动释放,必须调用 DeleteGlobalRef 手动释放。
弱全局引用
调用 NewWeakGlobalRef 基于局部引用或全局引用创建,它不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。在 JVM 认为应该回收它的时候(比如内存紧张时)会进行回收而被释放,或调用 DeleteWeakGlobalRef 手动释放。
JNI函数注册
静态注册
对于如下代码:
1 | public class Main { |
执行命令:
1 | javac Main.java |
第二个命令会在当前目录中生成 Main.h 文件,如下所示:
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
然后新建 Main.c 文件:
1 |
|
接着通过 gcc -I $JAVA_HOME/include/ -I $JAVA_HOME/include/darwin -fPIC -shared -o libJNIDemo.so Main.c
命令生成目标 so 文件,最后执行。
当我们在 Java 中调用 hello 方法时,就会从 JNI 中寻找 Java_Main_hello 方法,如果没有就会报错,如果找到就会为其建立关联,其实是保存 JNI 的方法指针,这样再次调用 hello 方法时就会直接通过这个方法指针来找到对应 JNI 方法。
静态注册就是根据方法名,将 Java 的 native 方法和 JNI 方法建立关联,而 JNI 层的方法名称过长,且初次调用 native 方法时需要建立关联,影响效率。
动态注册
静态注册是 Java 的 native 方法通过方法指针来与 JNI 进行关联,如果 Native 方法知道它在 JNI 中对应的方法指针,就可以避免上述的缺点了,这就是动态注册。
1 |
|
JNI数据类型对应
JAVA类型 | JNI类型 | 描述 |
---|---|---|
int | jint/jsize | signed 32 bits |
long | jlong | signed 64 bits |
byte | jbyte | signed 8 bits |
boolean | jboolean | unsigned 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
Java对象做为引用被传递到本地方法中,所有这些Java对象的引用都有一个共同的父类型jobject(相当于java中的Object类是所有类的父类一样)。
String对象
从 Java 程序中传过去的 String 对象在 JNI 方法中对应的是 jstring 类型,jstring 和 C 中的 char*
不同,如果直接当做 char*
使用会出错。在使用之前需要将 jstring 转换成为 c/c++
中的 char*
:
1 | char buf[128]; |
下面是访问 String 的一些方法:
- GetStringUTFChars: 将 jstring 转换成为 UTF-8 格式的
char*
- GetStringChars: 将 jstring 转换成为 Unicode 格式的
char*
- ReleaseStringUTFChars: 释放指针
- ReleaseStringChars: 释放指针
- NewStringUTF: 创建一个 UTF-8 格式的 jstring 对象
- NewString: 创建一个 Unicode 格式的 jstring 对象
- GetStringUTFLength: 获取 UTF-8 格式的
char*
的长度 - GetStringLength: 获取 Unicode 格式的
char*
的长度
Java原始类型数组
1、获取数组的长度
1 | JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { |
2、获取一个指向数组元素的指针
1 | // 第三个参数是数组里面开始的元素 |
3、使用指针取出Array中的元素
1 | for (i=0; i<len; i++) { |
4、释放数组元素的引用
1 | (*env)->ReleaseIntArrayElements(env, arr, body, 0); |
获取函数 | 释放函数 | 数组类型 |
---|---|---|
GetBooleanArrayElements | ReleaseBooleanArrayElements | boolean |
GetByteArrayElements | ReleaseByteArrayElements | byte |
GetCharArrayElements | ReleaseCharArrayElements | char |
GetShortArrayElements | ReleaseShortArrayElements | short |
GetIntArrayElements | ReleaseIntArrayElements | int |
GetLongArrayElements | ReleaseLongArrayElements | long |
GetFloatArrayElements | ReleaseFloatArrayElements | float |
GetDoubleArrayElements | ReleaseDoubleArrayElements | double |
Java自定义对象数组
JNI 提供了一组单独的函数来访问对象数组的元素,我们可以使用这些函数来获取和设置单独的对象数组元素。
- GetObjectArrayElement: 在给定的索引中返回对象元素。
- SetObjectArrayElement: 在给定索引处更新对象元素。
Java对象
在本地方法中调用Java对象的方法的步骤:
1、获取你需要访问的Java对象的类:
1 | jclass cls = (*env)->GetObjectClass(obj); |
2、获取MethodID:
1 | // env-->JNIEnv |
3、调用方法:
1 | // env-->JNIEnv |
注:这里使用的是 CallVoidMethod 方法调用,因为没有返回值,如果有返回值的话使用对应的方法,在后面会提到。
Java 中参数类型和其对应的签名值如下:
Signature | Java中的类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
L | long |
F | float |
D | double |
V | void |
L 开头, ; 结尾, / 隔开包和类名, 嵌套类使用$ . Ljava/lang/String;Lxx/xx$$Class |
object对象 |
一个Java类的方法的 Signature 可以通过 javap 命令获取。
通常我们直接在 methodID 后面将要传的参数添加在后面,但是还有其他的方法也可以传参数:
- CallVoidMethodV: 可以获取一个数量可变的列表作为参数
- CallVoidMethodA: 可以获取一个union
调用静态方法:
- GetStaticMethodID: 获取对应的静态方法的ID
- CallStaticIntMethod: 调用静态方法
Java对象的属性
访问 Java 对象的属性和访问 Java 对象的方法基本上一样,只需要将函数里面的 Method 改为 Field 即可。
集合对象
1 | // 获得 ArrayList 类引用 |