JNI将本地代码使用的对象引用分为两类:
局部引用
和全局引用
。
局部引用在本地方法调用期间有效,并在方法返回后自动释放。全局引用在显式释放之前一直保持有效
Java对象会作为局部引用传递给本地方法,JNI函数返回的所有Java对象也都是局部引用,但JNI允许程序员从局部引用创建全局引用。由于局部引用的特点,它不能跨线程、跨方法共享。
NewObject/FindClass/NewStringUTF
等等函数创建的都是局部引用,要注意,不能在本地方法中把局部引用存储在静态变量中,以供下一次调用时使用
JNIEXPORT jstring JNICALL
Test(JNIEnv *env, jobject instance) {
//错误!!! 第二次执行时, str引用的内存已被释放
static jstring str;
if(str == NULL){
str = (*env)->NewStringUTF(env,"这是字符串");
}
return str;
}
局部引用的释放
有两种释放方式
本地方法执行完毕后会自动释放
通过DeleteLocalRef
函数手动释放
既然可以自动释放,为什么还要手动释放?
为了实现局部引用,Java VM会创建一个注册表。注册表将不可移动的局部引用映射到Java对象,并防止垃圾回收对象。传递给本地方法的所有Java对象(包括那些作为JNI函数返回结果的Java对象)都将自动添加到注册表中。本地方法返回后,注册表将被删除,从而允许对其所有条目进行垃圾回收。
当本地方法中创建大量局部引用时,尽管并非同时使用所有的局部引用,但由于调用到其他方法中,导致大量局部引用不能被及时回收,因此可能会导致系统内存不足。尤其是在安卓设备中,系统分配给每个进程的内存都是有限的,在函数中手动释放局部引用,提升内存的使用效率。
函数原型
void DeleteLocalRef(JNIEnv *env, jobject localRef);
全局引用又可分为普通全局引用和弱全局引用
它可以跨方法、跨线程共享,直到被手动释放才会失效。
jstring globalStr;
JNIEXPORT jstring JNICALL
Java_com_jnitest_func(JNIEnv *env, jobject instance) {
if(globalStr == NULL){
// 局部引用
jstring str = (*env)->NewStringUTF(env,"这是字符串");
// 从局部引用创建全局引用
globalStr = (jstring)(*env)->NewGlobalRef(env,str);
}
//释放全局引用
(*env)->DeleteGlobalRef(env,str);
return globalStr;
}
函数原型
jobject NewGlobalRef(JNIEnv *env, jobject obj);
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
与全局引用类似,弱全局引用可以跨方法、跨线程共享,不同之处在于弱全局引用不会阻止Java 的垃圾回收,当Java GC执行垃圾回收时,弱全局引用就会被释放。因此,每次使用弱全局引用时,都要检查其是否仍然有效。
JNIEXPORT jclass JNICALL
Java_com_jnitest_func2(JNIEnv *env, jobject instance) {
static jclass globalClazz = NULL;
//检查有效性
jboolean isFlags = env->IsSameObject(env,globalClazz, NULL);
if (globalClazz == NULL || isFlags) {
// 从Java实例对象获取class对象
jclass clazz = (*env)->GetObjectClass(env, instance);
//创建弱全局引用
globalClazz = (jclass)(*env)->NewWeakGlobalRef(env,clazz);
(*env)->DeleteLocalRef(env, clazz);
}
return globalClazz;
}
函数原型
// 判断两个引用是否指向相同的Java对象
jboolean IsSameObject(JNIEnv *env, jobject ref1,jobject ref2);
// 创建弱全局引用
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
// 释放弱全局引用
void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
C语言本身是没有异常处理机制的,因此JNI中的所谓异常处理,是指本地的C语言代码反射调用Java方法时,在Java方法中发生异常的处理方式。
示例
Java代码
public class JniUtil {
static {
System.load("D:\\workspace\\c_code\\ndk\\libtest.dll");
}
// 该方法引发一个除数为0的异常
public static void div() {
System.out.println(8/0);
}
public static native void jniCall();
}
本地C代码
JNIEXPORT void JNICALL Java_com_test_JniUtil_jniCall(JNIEnv *env, jclass jclz){
jthrowable exc = NULL;
jmethodID jMid = (*env)->GetStaticMethodID(env,jclz,"div","()V");
if (jMid != NULL) {
// 调用Java类中的div()方法,引发一个异常
(*env)->CallStaticVoidMethod(env,jclz,jMid);
}
// 检查当前是否发生了异常
exc = (*env)->ExceptionOccurred(env);
if (exc) {
(*env)->ExceptionDescribe(env); // 打印Java层抛出的异常堆栈信息
(*env)->ExceptionClear(env); // 清除异常信息
// 抛出自己的异常处理
jclass newExcClz = (*env)->FindClass(env,"java/lang/Exception");
if (newExcClz == NULL) {
return;
}
(*env)->ThrowNew(env, newExcClz, "JNICALL: from C Code!");
return;
}
// do samething ...
}
因为原生函数的代码执行不受虚拟机控制,因此抛出异常后并不会停止原生函数的执行,也不会把控制权转交给Java异常处理程序,所以当发送异常时,我们需要手动去处理,例如相关资源的释放,避免内存泄露,以及控制何时返回,不往下执行了。
相关函数原型
// 确定是否引发异常,没有异常时返回NULL
jthrowable ExceptionOccurred(JNIEnv *env);
// 打印异常堆栈信息
void ExceptionDescribe(JNIEnv *env);
// 清除当前引发的任何异常
void ExceptionClear(JNIEnv *env);
// 从指定的类构造一个异常对象
jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
之前编写Java代码的native方法时,在JNI实现中,都需要一种特殊的方法签名与之对应,也就是包名+类名的形式,这使得JNI实现中的C语言函数的函数名都非常的长,可读性也比较差,实际上在JNI中,还有一种动态注册的方式来实现Java的native方法与JNI实现函数的关联。
在动态注册之前,我们需要了解一个函数JNI_OnLoad
,它为我们提供了动态注册本地方法的时机。该方法在动态库被加载时(如System.loadLibrary
)自动调用。 JNI_OnLoad
必须返回本地库所需的JNI版本,且必须大于JNI_VERSION_1_1
,如JNI_VERSION_1_2
、JNI_VERSION_1_4
、JNI_VERSION_1_6
jint JNI_OnLoad(JavaVM *vm, void *reserved);
示例
Java 代码
package com.test;
public class JniUtil {
static {
System.load("D:\\workspace\\c_code\\ndk\\libtest.dll");
}
public static native void javaMet1();
public static native String javaMet2(byte b[]);
}
C代码
#include
#include
#include
#include
void method1(JNIEnv *env, jclass jclz){
printf("hello,from C!\n");
}
jstring method2(JNIEnv *env, jclass jclz,jbyteArray jbyteArr){
jbyte *byts = (*env)->GetByteArrayElements(env,jbyteArr,NULL);
if(byts == NULL){
return 0;
}
char buf[100]={0};
jsize len = (*env)->GetArrayLength(env,jbyteArr);
memcpy(buf,byts,len);
return (*env)->NewStringUTF(env, buf);
}
//需要动态注册的方法数组
static const JNINativeMethod methods[] = {
{"javaMet1","()V", (void*)method1 },
{"javaMet2", "([B)Ljava/lang/String;", (jstring*)method2 }
};
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
//获得 JniEnv
int ret = (*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_6);
if( ret != JNI_OK){
return -1;
}
// 需要动态注册native方法的类
jclass clz = (*env)->FindClass(env, "com/test/JniUtil");
// 检查是否注册成功
ret = (*env)->RegisterNatives(env,clz,methods,sizeof(methods)/sizeof(JNINativeMethod));
if(ret != JNI_OK){
return -1;
}
return JNI_VERSION_1_6;
}
测试代码
public static void main(String[] args) {
JniUtil.javaMet1();
System.out.println(JniUtil.javaMet2("Java String".getBytes()));
}
JNI函数原型
// 获取当前线程的 JNIEnv
jint GetEnv(JavaVM *vm, void **env, jint version);
// 注册本地方法(最后两个参数分别为JNINativeMethod结构体数组,以及数组的长度)
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
// JNINativeMethod 结构体
typedef struct {
char *name; // Java的native方法名
char *signature; // 方法签名
void *fnPtr; // 对应的JNI本地函数的指针
} JNINativeMethod;
JNI中可以使用JVM的线程,也可以使用本地的POSIX线程。JNI还提供了同步锁,来处理多线程的并发访问。
(*env)->MonitorEnter(env,obj);
// 线程同步代码块
*env)->MonitorExit(env, obj)
其作用等同于Java中的synchronized
代码块
synchronized (obj) {
// 线程同步代码块
}
需要注意,在原生的POSIX线程中使用同步锁时,该线程必须附着到Java虚拟机上,且锁对象obj
必须是Java对象,另外MonitorEnter
和MonitorExit
必须成对出现。
原型
jint MonitorEnter(JNIEnv *env, jobject obj);
jint MonitorExit(JNIEnv *env, jobject obj);
JNIEnv
是和线程相关的,每个线程都有自己的JNIEnv
,因此不应该将JNIEnv
缓存起来,并在不同的线程中传递。要想获取当前线程的JNIEnv
,可以使用JavaVM
的GetEnv
函数获取,而要想获取JavaVM
,建议在JNI_OnLoad
函数中缓存一个全局的JavaVM
实例。
另外,在使用原生的POSIX线程时,如果该线程未附着到Java虚拟机,则无法反射调用Java的方法,无获得JNIEnv
对象。因为Java虚拟机并不知道原生线程,所以原生线程是无法与Java通信的。
JavaVM* cacheJvm;
// ......
JNIEnv* env;
// ......
// 将当前线程附着到Java虚拟机
(*cacheJvm)->AttachCurrentThread(cacheJvm,&env,NULL);
// do samething
// 将当前线程与虚拟机分离
(*cacheJvm)->DetachCurrentThread(cacheJvm);