Java 之JNI进阶篇(四)

JNI 本地对象的引用类型

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

局部引用的释放

有两种释放方式

  1. 本地方法执行完毕后会自动释放

  2. 通过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);

JNI 异常处理

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_2JNI_VERSION_1_4JNI_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 <jni.h>
#include <jni_md.h>
#include <stdio.h> 
#include <string.h>


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 中的线程

JNI中可以使用JVM的线程,也可以使用本地的POSIX线程。JNI还提供了同步锁,来处理多线程的并发访问。

同步

(*env)->MonitorEnter(env,obj);
// 线程同步代码块
*env)->MonitorExit(env, obj) 

其作用等同于Java中的synchronized代码块

synchronized (obj) {
// 线程同步代码块
}

需要注意,在原生的POSIX线程中使用同步锁时,该线程必须附着到Java虚拟机上,且锁对象obj必须是Java对象,另外MonitorEnterMonitorExit必须成对出现。

原型

jint MonitorEnter(JNIEnv *env, jobject obj);
jint MonitorExit(JNIEnv *env, jobject obj);

线程的注意事项

JNIEnv是和线程相关的,每个线程都有自己的JNIEnv,因此不应该将JNIEnv缓存起来,并在不同的线程中传递。要想获取当前线程的JNIEnv,可以使用JavaVMGetEnv函数获取,而要想获取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);

猜你喜欢

转载自blog.csdn.net/yingshukun/article/details/102149146