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;
}
局部引用的釋放
有兩種釋放方式
-
本地方法執行完畢後會自動釋放
-
通過
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_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 <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對象,另外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);