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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章