在 Native 代碼中有時候會接收 Java 傳入的引用類型參數,有時候也會經過 NewObject 方法來建立一個 Java 的引用類型變量。java
在編寫 Native 代碼時,要注意這個表明 Java 數據結構類型的引用在使用時會被 GC 回收的可能性。android
Native 代碼並不能直接經過引用來訪問其內部的數據接口,必需要經過調用 JNI 接口來間接操做這些引用對象,就如在以前的系列文章中寫的那樣。而且 JNI 還提供了和 Java 相對應的引用類型,所以,咱們就須要經過管理好這些引用來管理 Java 對象,避免在使用時被 GC 回收了。git
JNI 提供了三種引用類型:github
局部引用是最多見的一種引用。絕大多數 JNI 函數建立的都是局部引用,好比:NewObject、FindClass、NewObjectArray 函數等等。緩存
局部引用會阻止 GC 回收所引用的對象,同時,它不能在本地函數中跨函數傳遞,不能跨線程使用。
局部引用在 Native 函數返回後,所引用的對象會被 GC 自動回收,也能夠經過 DeleteLocalRef 函數來手動回收。微信
在以前文章 JNI 調用時緩存字段和方法 ID,第一種方法採用的是使用時緩存,把字段 ID 經過 static 變量緩存起來。數據結構
若是把 FindClass 函數建立的局部引用也經過 static 變量緩存起來,那麼在函數退出後,局部引用被自動釋放了,static 靜態變量中存儲的就是一個被釋放後的內存地址,成爲了一個野指針,再次調用時就會引發程序崩潰了。函數
extern "C" JNIEXPORT jstring JNICALL Java_com_glumes_cppso_jnioperations_LocalAndGlobalReferenceOps_errorCacheUseLocalReference( JNIEnv *env, jobject instance) { static jmethodID mid = NULL; static jclass cls; // 局部引用不能使用 static 來緩存,不然函數退出後,自動釋放,成爲野指針,程序 Crash if (cls == NULL) { cls = env->FindClass("java/lang/String"); if (cls == NULL) { return NULL; } } else { LOGD("cls is not null but program will crash"); } if (mid == NULL) { mid = env->GetMethodID(cls, "<init>", "([C)V"); if (mid == NULL) { return NULL; } } jcharArray charEleArr = env->NewCharArray(10); const jchar *j_char = env->GetStringChars(env->NewStringUTF("LocalReference"), NULL); env->SetCharArrayRegion(charEleArr, 0, 10, j_char); jstring result = (jstring) env->NewObject(cls, mid, charEleArr); env->DeleteLocalRef(charEleArr); return result; }
局部引用除了自動釋放外,還能夠經過 DeleteLocalRef 函數手動釋放,它通常存在於如下場景中:工具
假若有如下代碼,則要特別注意,及時釋放局部引用,防止溢出。post
for (int i = 0; i < len; ++i) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* process jstr */ (*env)->DeleteLocalRef(env, jstr); }
在編寫工具類時,很難知道被調用者具體會是誰,考慮到通用性,完成工具類的任務以後,就要及時釋放相應的局部引用,防止被佔着內存空間。
若是 Native 方法不會返回,那麼自動釋放局部引用就失效了,這時候就必需要手動釋放。好比,在某個一直等待的循環中,若是不及時釋放局部引用,很快就會溢出了。
好比,經過局部引用建立了一個大對象,而後這個對象在函數中間就完成了任務,那麼就能夠早早地經過手動釋放了,而不是等到函數的結尾才釋放。
Java 還提供了一些函數來管理局部引用的生命週期:
JNI 的規範指出,JVM 要確保每一個 Native 方法至少能夠建立 16 個局部引用,經驗代表,16 個局部引用已經足夠日常的使用了。
可是,若是要與 JVM 的中對象進行復雜的交互計算,就須要建立更多的局部引用了,這時就須要使用 EnsureLocalCapacity
來確保能夠建立指定數量的局部引用,若是建立成功返回 0 ,返回返回小於 0 ,以下代碼示例:
// Use EnsureLocalCapacity int len = 20; if (env->EnsureLocalCapacity(len) < 0) { // 建立失敗,out of memory } for (int i = 0; i < len; ++i) { jstring jstr = env->GetObjectArrayElement(arr,i); // 處理 字符串 // 建立了足夠多的局部引用,這裏就不用刪除了,顯然佔用更多的內存 }
引用確保能夠建立了足夠的局部引用數量,因此在循環處理局部引用時能夠不進行刪除了,可是顯然會消耗更多的內存空間了。
PushLocalFrame 與 PopLocalFrame 是兩個配套使用的函數對。
它們能夠爲局部引用建立一個指定數量內嵌的空間,在這個函數對之間的局部引用都會在這個空間內,直到釋放後,全部的局部引用都會被釋放掉,不用再擔憂每個局部引用的釋放問題了。
常見的使用場景就是在循環中:
// Use PushLocalFrame & PopLocalFrame for (int i = 0; i < len; ++i) { if (env->PushLocalFrame(len)) { // 建立指定數據的局部引用空間 //out ot memory } jstring jstr = env->GetObjectArrayElement(arr, i); // 處理字符串 // 期間建立的局部引用,都會在 PushLocalFrame 建立的局部引用空間中 // 調用 PopLocalFrame 直接釋放這個空間內的全部局部引用 env->PopLocalFrame(NULL); }
使用 PushLocalFrame & PopLocalFrame 函數對,就能夠在期間放心地處理局部引用,最後統一釋放掉。
全局引用和局部引用同樣,也會阻止它所引用的對象被回收。可是它不會在方法返回時被自動釋放,必需要經過手動釋放才行,並且,全局引用能夠跨方法、跨線程使用。
全局引用只能經過 NewGlobalRef
函數來建立,而後經過 DeleteGlobalRef
函數來手動釋放。
仍是上面提到的緩存字段的例子,如今就可使用全局引用來緩存了。
extern "C" JNIEXPORT jstring JNICALL Java_com_glumes_cppso_jnioperations_LocalAndGlobalReferenceOps_cacheWithGlobalReference(JNIEnv *env, jobject instance) { static jclass stringClass = NULL; if (stringClass == NULL) { jclass localRefs = env->FindClass("java/lang/String"); if (localRefs == NULL) { return NULL; } stringClass = (jclass) env->NewGlobalRef(localRefs); env->DeleteLocalRef(localRefs); if (stringClass == NULL) { return NULL; } } else { LOGD("use stringClass cached"); } static jmethodID stringMid = NULL; if (stringMid == NULL) { stringMid = env->GetMethodID(stringClass, "<init>", "(Ljava/lang/String;)V"); if (stringMid == NULL) { return NULL; } } else { LOGD("use method cached"); } jstring str = env->NewStringUTF("string"); return (jstring) env->NewObject(stringClass, stringMid, str); }
弱全局引用有點相似於 Java 中的弱引用,它所引用的對象能夠被 GC 回收,而且它也能夠跨方法、跨線程使用。
弱引用使用 NewWeakGlobalRef
方法建立,使用 DeleteWeakGlobalRef
方法釋放。
extern "C" JNIEXPORT void JNICALL Java_com_glumes_cppso_jnioperations_LocalAndGlobalReferenceOps_useWeakGlobalReference(JNIEnv *env, jobject instance) { static jclass stringClass = NULL; if (stringClass == NULL) { jclass localRefs = env->FindClass("java/lang/String"); if (localRefs == NULL) { return; } stringClass = (jclass) env->NewWeakGlobalRef(localRefs); if (stringClass == NULL) { return; } } static jmethodID stringMid = NULL; if (stringMid == NULL) { stringMid = env->GetMethodID(stringClass, "<init>", "(Ljava/lang/String;)V"); if (stringMid == NULL) { return; } } jboolean isGC = env->IsSameObject(stringClass, NULL); if (isGC) { LOGD("weak reference has been gc"); } else { jstring str = (jstring) env->NewObject(stringClass, stringMid, env->NewStringUTF("jstring")); LOGD("str is %s", env->GetStringUTFChars(str, NULL)); } }
在使用弱引用時,要先檢查弱引用所指的類對象是否被 GC 給回收了。經過 isSameObject
方法進行檢查。
isSameObject
方法能夠用來比較兩個引用類型是否相同,也能夠用來比較引用是否爲 NULL。同時,還能夠用 isSameObject
來比較弱全局引用所引用的對象是否被 GC 了,返回 JNI_TRUE 則表示回收了,JNI_FALSE 則表示未被回收。
env->IsSameObject(obj1, obj2) // 比較局部引用 和 全局引用是否相同 env->IsSameObject(obj, NULL) // 比較局部引用或者全局引用是否爲 NULL env->IsSameObject(wobj, NULL) // 比較弱全局引用所引用對象是否被 GC 回收
總結一些關於引用管理方面的知識點,能夠減小內存的使用和避免由於對象被引用不能釋放而形成的內存浪費。
一般來講,Native 代碼大致有兩種狀況:
對於直接實現 Java 層聲明的 Native 函數,不要形成全局引用和弱全局引用的累加,由於它們在函數返回後並不會自動釋放。
對於工具類的 Native 函數,因爲它的調用場合和次數是不肯定的,因此要格外當心各類引用類型,避免形成累積而致使內存溢出,好比以下規則:
同時,對於工具類的 Native 函數,使用緩存技術來保存一些全局引用也是可以提升效率的,正如 Android JNI 調用時緩存字段和方法 ID 文章中寫到的同樣。同時,在工具類中,若是返回的是引用類型,最好說明返回的引用是哪種類型,以下代碼所示:
while (JNI_TRUE) { jstring infoString = GetInfoString(info); ... /* process infoString */ ??? /* we need to call DeleteLocalRef, DeleteGlobalRef, or DeleteWeakGlobalRef depending on the type of reference returned by GetInfoString. */ }
由於不清楚返回的引用是什麼類型,致使不知道調用哪一種方式來刪除這個引用類型。
JNI 中提供了 NewLocalRef
函數來保證工具類函數返回的老是一個局部引用類型,以下代碼:
static jstring cachedString = NULL; if (cachedString == NULL) { /* create cachedString for the first time */ jstring cachedStringLocal = ... ; /* cache the result in a global reference */ cachedString =(*env)->NewGlobalRef(env, cachedStringLocal); } return (*env)->NewLocalRef(env, cachedString);
正如以前提到的,static 緩存是不能緩存一個局部引用的,而 cachedString 正是一個緩存的全局引用,可是經過 NewLocalRef 函數確保將返回的是一個局部引用,這樣再使用後局部引用就會被自動釋放掉了。
對於引用的管理,最好的方式仍是使用 PushLocalFrame
與 PopLocalFrame
函數對,在這個函數對之間的局部引用就能夠自動被 PushLocalFrame 和 PopLocalFrame 接管了。
具體示例代碼可參考個人 Github 項目,歡迎 Star。
https://github.com/glumes/AndroidDevWithCpp
歡迎關注微信公衆號:【紙上淺談】,得到最新文章推送~~