JNI/NDK開發指南(十)——JNI區域性引用、全域性引用和弱全域性引用
這篇文章比較偏理論,詳細介紹了在編寫原生代碼時三種引用的使用場景和注意事項。可能看起來有點枯燥,但引用是在JNI中最容易出錯的一個點,如果使用不當,容易使程式造成記憶體溢位,程式崩潰等現象。所以講得比較細,有些地方看起來可能比較囉嗦,還請輕啪!《Android JNI區域性引用表溢位:local reference table overflow (max=512)》這篇文章是一個JNI引用使用不當造成引用表溢位,最終導致程式崩潰的例子。建議看完這篇文章之後,再去看。
做Java的朋友都知道,在編碼的過程當中,記憶體管理這一塊完全是透明的。new一個類的例項時,只知道建立完這個類的例項之後,會返回這個例項的一個引用,然後就可以拿著這個引用訪問它的所有資料成員了(屬性、方法)。完全不用管JVM內部是怎麼實現的,如何為新建立的物件來申請記憶體,也不用管物件使用完之後記憶體是怎麼釋放的,只需知道有一個垃圾回器在幫忙管理這些事情就OK的了。有經驗的朋友也許知道啟動一個Java程式,如果沒有手動建立其它執行緒,預設會有兩個執行緒在跑,一個是main執行緒,另一個就是GC執行緒(負責將一些不再使用的物件回收)。如果你曾經是做Java的然後轉去做C++,會感覺很“蛋疼”,在C++中new一個物件,使用完了還要做一次delete操作,malloc一次同樣也要呼叫free來釋放相應的記憶體,否則你的程式就會有記憶體洩露了。而且在C/C++中記憶體還分棧空間和堆空間,其中區域性變數、函式形參變數、for中定義的臨時變數所分配的記憶體空間都是存放在棧空間(而且還要注意大小的限制),用new和malloc申請的記憶體都存放在堆空間。。。但C/C++裡的記憶體管理還遠遠不止這些,這些只是最基礎的記憶體管理常識。做Java的童鞋聽到這些肯定會偷樂了,咱寫Java的時候這些都不用管,全都交給GC就萬事無優了。手動管理記憶體雖然麻煩,而且需要特別細心,一不小心就有可能造成記憶體洩露和野指標訪問等程式致命的問題,但凡事都有利弊,手動申請和釋放記憶體對程式的掌握比較靈活,不會受到平臺的限制。比如我們寫Android程式的時候,記憶體使用就受Dalivk虛擬機器的限制,從最初版本的16~24M,到後來的32M到64M,可能隨著以後移動裝置實體記憶體的不大擴大,後面的Android版本記憶體限制可能也會隨著提高。但在C/C++這層,就完全不受虛擬機器的限制了。比如要在Android中要儲存一張超高清的圖片,剛好這張圖片的大小超過了Dalivk虛擬機器對每個應用的記憶體大小限制,Java此時就顯得無能為力了,但在C/C++看來就是小菜一碟了,malloc(1024*1024*50),要多少記憶體,您說個數。。。C/C++程式設計師得意的說道~~Java不是說是一門純面象物件的語言嗎,所以除了基本資料型別外,其它任何型別所建立的物件,JVM所申請的記憶體都存在堆空間。上面提高到了GC,是負責回收不再使用的物件,它的全稱是Garbage Collection,也就是所謂的垃圾回收。JVM會在適當的時機觸發GC操作,一旦進行GC操作,就會將一些不再使用的物件進行回收。那麼哪些物件會被認為是不再使用,並且可以被回收的呢?我們來看下面二張圖:(注:圖摘自博主郭霖的
上圖當中,每個藍色的圓圈就代表一個記憶體當中的物件,而圓圈之間的箭頭就是它們的引用關係。這些物件有些是處於活動狀態的,而有些就已經不再被使用了。那麼GC操作會從一個叫作Roots的物件開始檢查,所有它可以訪問到的物件就說明還在使用當中,應該進行保留,而其它的物件就表示已經不再被使用了,如下圖所示:
可以看到,目前所有黃色的物件都處於活動狀態,仍然會被系統繼續保留,而藍色的物件就會在GC操作當中被系統回收掉了,這就是JVM執行一次GC的簡單流程。
上面說的廢話好像有點多哈,下面進入正題。通過上面的討論,大家都知道,如果一個Java物件沒有被其它成員變數或靜態變數所引用的話,就隨時有可能會被GC回收掉。所以我們在編寫原生代碼時,要注意從JVM中獲取到的引用在使用時被GC回收的可能性。由於原生代碼不能直接通過引用操作JVM內部的資料結構,要進行這些操作必須呼叫相應的JNI介面來間接操作所引用的資料結構。JNI提供了和Java相對應的引用型別,供原生代碼配合JNI介面間接操作JVM內部的資料內容使用。如:jobject、jstring、jclass、jarray、jintArray等。因為我們只通過JNI介面操作JNI提供的引用型別資料結構,而且每個JVM都實現了JNI規範相應的介面,所以我們不必擔心特定JVM中物件的儲存方式和內部資料結構等資訊,我們只需要學習JNI中三種不同的引用即可。
由於Java程式執行在虛擬機器中的這個特點,在Java中建立的物件、定義的變數和方法,內部物件的資料結構是怎麼定義的,只有JVM自己知道。如果我們在C/C++中想要訪問Java中物件的屬性和方法時,是不能夠直接操作JVM內部Java物件的資料結構的。想要在C/C++中正確的訪問Java的資料結構,JVM就必須有一套規則來約束C/C++與Java互相訪問的機制,所以才有了JNI規範,JNI規範定義了一系列介面,任何實現了這套JNI介面的Java虛擬機器,C/C++就可以通過呼叫這一系列介面來間接的訪問Java中的資料結構。比如前面文章中學習到的常用JNI介面有:GetStringUTFChars(從Java虛擬機器中獲取一個字串)、ReleaseStringUTFChars(釋放從JVM中獲取字串所分配的記憶體空間)、NewStringUTF、GetArrayLength、GetFieldID、GetMethodID、FindClass等。
三種引用簡介及區別
在JNI規範中定義了三種引用:區域性引用(Local Reference)、全域性引用(Global Reference)、弱全域性引用(Weak Global Reference)。區別如下:
1、區域性引用:通過NewLocalRef和各種JNI介面建立(FindClass、NewObject、GetObjectClass和NewCharArray等)。會阻止GC回收所引用的物件,不在本地函式中跨函式使用,不能跨線前使用。函式返回後區域性引用所引用的物件會被JVM自動釋放,或呼叫DeleteLocalRef釋放。(*env)->DeleteLocalRef(env,local_ref)
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj); // 通過NewLocalRef函式建立
...
2、全域性引用:呼叫NewGlobalRef基於區域性引用建立,會阻GC回收所引用的物件。可以跨方法、跨執行緒使用。JVM不會自動釋放,必須呼叫DeleteGlobalRef手動釋放(*env)->DeleteGlobalRef(env,g_cls_string);
static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
g_cls_string = (*env)->NewGlobalRef(env,cls_string);
}
3、 弱全域性引用:呼叫NewWeakGlobalRef基於區域性引用或全域性引用建立,不會阻止GC回收所引用的物件,可以跨方法、跨執行緒使用。引用不會自動釋放,在JVM認為應該回收它的時候(比如記憶體緊張的時候)進行回收而被釋放。或呼叫DeleteWeakGlobalRef手動釋放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)
static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}
區域性引用
區域性引用也稱本地引用,通常是在函式中建立並使用。會阻止GC回收所引用的物件。比如,呼叫NewObject介面建立一個新的物件例項並返回一個對這個物件的區域性引用。區域性引用只有在建立它的本地方法返回前有效,本地方法返回到Java層之後,如果Java層沒有對返回的區域性引用使用的話,區域性引用就會被JVM自動釋放。你可能會為了提高程式的效能,在函式中將區域性引用儲存在靜態變數中快取起來,供下次呼叫時使用。這種方式是錯誤的,因為函式返回後區域性引很可能馬上就會被釋放掉,靜態變數中儲存的就是一個被釋放後的記憶體地址,成了一個野針對,下次再使用的時候就會造成非法地址的訪問,使程式崩潰。請看下面一個例子,錯誤的快取了String的Class引用:
/*錯誤的區域性引用*/
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
jcharArray elemArray;
jchar *chars = NULL;
jstring j_str = NULL;
static jclass cls_string = NULL;
static jmethodID cid_string = NULL;
// 注意:錯誤的引用快取
if (cls_string == NULL) {
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
// 快取String的構造方法ID
if (cid_string == NULL) {
cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
if (cid_string == NULL) {
return NULL;
}
}
//省略額外的程式碼.......
elemArray = (*env)->NewCharArray(env, len);
// ....
j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
// 釋放區域性引用
(*env)->DeleteLocalRef(env, elemArray);
return j_str;
}
上面程式碼中,我們省略了和我們討論無關的程式碼。因為FindClass返回一個對java.lang.String物件的區域性引用,上面程式碼中快取cls_string做法是錯誤的。假設一個本地方法C.f呼叫了newString:
JNIEXPORT jstring JNICALL
Java_C_f(JNIEnv *env, jobject this)
{
char *c_str = ...;
...
return newString(c_str);
}
Java_com_study_jnilearn_AccessCache_newString 下面簡稱newString
C.f方法返回後,JVM會釋放在這個方法執行期間建立的所有區域性引用,也包含對String的Class引用cls_string。當再次呼叫newString時,newString所指向引用的記憶體空間已經被釋放,成為了一個野指標,再訪問這個指標的引用時,會導致因非法的記憶體訪問造成程式崩潰。
...
... = C.f(); // 第一次調是OK的
... = C.f(); // 第二次呼叫時,訪問的是一個無效的引用.
...
釋放區域性引用
釋放一個區域性引用有兩種方式,一個是本地方法執行完畢後JVM自動釋放,另外一個是自己呼叫DeleteLocalRef手動釋放。既然JVM會在函式返回後會自動釋放所有區域性引用,為什麼還需要手動釋放呢?大部分情況下,我們在實現一個本地方法時不必擔心區域性引用的釋放問題,函式被呼叫完成後,JVM 會自動釋放函式中建立的所有區域性引用。儘管如此,以下幾種情況下,為了避免記憶體溢位,我們應該手動釋放區域性引用:
1、JNI會將建立的區域性引用都儲存在一個區域性引用表中,如果這個表超過了最大容量限制,就會造成區域性引用表溢位,使程式崩潰。經測試,Android上的JNI區域性引用表最大數量是512個。當我們在實現一個本地方法時,可能需要建立大量的區域性引用,如果沒有及時釋放,就有可能導致JNI區域性引用表的溢位,所以,在不需要區域性引用時就立即呼叫DeleteLocalRef手動刪除。比如,在下面的程式碼中,原生代碼遍歷一個特別大的字串陣列,每遍歷一個元素,都會建立一個區域性引用,當對使用完這個元素的區域性引用時,就應該馬上手動釋放它。
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* 使用jstr */
(*env)->DeleteLocalRef(env, jstr); // 使用完成之後馬上釋放
}
2、在編寫JNI工具函式時,工具函式在程式當中是公用的,被誰呼叫你是不知道的。上面newString這個函式演示了怎麼樣在工具函式中使用完區域性引用後,呼叫DeleteLocalRef刪除。不這樣做的話,每次呼叫newString之後,都會遺留兩個引用佔用空間(elemArray和cls_string,cls_string不用static快取的情況下)。
3、如果你的本地函式不會返回。比如一個接收訊息的函式,裡面有一個死迴圈,用於等待別人傳送訊息過來while(true) { if (有新的訊息) { 處理之。。。。} else { 等待新的訊息。。。}}
。如果在訊息迴圈當中建立的引用你不顯示刪除,很快將會造成JVM區域性引用表溢位。
4、區域性引用會阻止所引用的物件被GC回收。比如你寫的一個本地函式中剛開始需要訪問一個大物件,因此一開始就建立了一個對這個物件的引用,但在函式返回前會有一個大量的非常複雜的計算過程,而在這個計算過程當中是不需要前面建立的那個大物件的引用的。但是,在計算的過程當中,如果這個大物件的引用還沒有被釋放的話,會阻止GC回收這個物件,記憶體一直佔用者,造成資源的浪費。所以這種情況下,在進行復雜計算之前就應該把引用給釋放了,以免不必要的資源浪費。
/* 假如這是一個本地方法實現 */
JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
lref = ... /* lref引用的是一個大的Java物件 */
... /* 在這裡已經處理完業務邏輯後,這個物件已經使用完了 */
(*env)->DeleteLocalRef(env, lref); /* 及時刪除這個對這個大物件的引用,GC就可以對它回收,並釋放相應的資源*/
lengthyComputation(); /* 在裡有個比較耗時的計算過程 */
return; /* 計算完成之後,函式返回之前所有引用都已經釋放 */
}
管理區域性引用
JNI提供了一系列函式來管理區域性引用的生命週期。這些函式包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI規範指出,任何實現JNI規範的JVM,必須確保每個本地函式至少可以建立16個區域性引用(可以理解為虛擬機器預設支援建立16個區域性引用)。實際經驗表明,這個數量已經滿足大多數不需要和JVM中內部物件有太多互動的本地方函式。如果需要建立更多的引用,可以通過呼叫EnsureLocalCapacity函式,確保在當前執行緒中建立指定數量的區域性引用,如果建立成功則返回0,否則建立失敗,並丟擲OutOfMemoryError異常。EnsureLocalCapacity這個函式是1.2以上版本才提供的,為了向下相容,在編譯的時候,如果申請建立的區域性引用超過了本地引用的最大容量,在執行時JVM會呼叫FatalError函式使程式強制退出。在開發過程當中,可以為JVM新增-verbose:jni引數,在編譯的時如果發現原生代碼在試圖申請過多的引用時,會列印警告資訊提示我們要注意。在下面的程式碼中,遍歷陣列時會獲取每個元素的引用,使用完了之後不手動刪除,不考慮記憶體因素的情況下,它可以為這種建立大量的區域性引用提供足夠的空間。由於沒有及時刪除區域性引用,因此在函式執行期間,會消耗更多的記憶體。
/*處理函式邏輯時,確保函式能建立len個區域性引用*/
if((*env)->EnsureLocalCapacity(env,len) != 0) {
... /*申請len個區域性引用的記憶體空間失敗 OutOfMemoryError*/
return;
}
for(i=0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
// ... 使用jstr字串
/*這裡沒有刪除在for中臨時建立的區域性引用*/
}
另外,除了EnsureLocalCapacity函式可以擴充指定容量的區域性引用數量外,我們也可以利用Push/PopLocalFrame函式對建立作用範圍層層巢狀的區域性引用。例如,我們把上面那段處理字串陣列的程式碼用Push/PopLocalFrame函式對重寫:
#define N_REFS ... /*最大區域性引用數量*/
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
... /*記憶體溢位*/
}
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* 使用jstr */
(*env)->PopLocalFrame(env, NULL);
}
PushLocalFrame為當前函式中需要用到的區域性引用建立了一個引用堆疊,(如果之前呼叫PushLocalFrame已經建立了Frame,在當前的本地引用棧中仍然是有效的)每遍歷一次呼叫(*env)->GetObjectArrayElement(env, arr, i);
返回一個區域性引用時,JVM會自動將該引用壓入當前區域性引用棧中。而PopLocalFrame負責銷燬棧中所有的引用。這樣一來,Push/PopLocalFrame函式對提供了對區域性引用生命週期更方便的管理,而不需要時刻關注獲取一個引用後,再呼叫DeleteLocalRef來釋放引用。在上面的例子中,如果在處理jstr的過程當中又建立了局部引用,則PopLocalFrame執行時,這些區域性引用將全都會被銷燬。在呼叫PopLocalFrame銷燬當前frame中的所有引用前,如果第二個引數result不為空,會由result生成一個新的區域性引用,再把這個新生成的區域性引用儲存在上一個frame中。請看下面的示例:
// 函式原型
jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);
jstring other_jstr;
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
... /*記憶體溢位*/
}
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* 使用jstr */
if (i == 2) {
other_jstr = jstr;
}
other_jstr = (*env)->PopLocalFrame(env, other_jstr); // 銷燬區域性引用棧前返回指定的引用
}
還要注意的一個問題是,區域性引用不能跨執行緒使用,只在建立它的執行緒有效。不要試圖在一個執行緒中建立區域性引用並存儲到全域性引用中,然後在另外一個執行緒中使用。
全域性引用
全域性引用可以跨方法、跨執行緒使用,直到它被手動釋放才會失效。同區域性引用一樣,也會阻止它所引用的物件被GC回收。與區域性引用建立方式不同的是,只能通過NewGlobalRef函式建立。下面這個版本的newString演示怎麼樣使用一個全域性引用:
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
// ...
jstring jstr = NULL;
static jclass cls_string = NULL;
if (cls_string == NULL) {
jclass local_cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
// 將java.lang.String類的Class引用快取到全域性引用當中
cls_string = (*env)->NewGlobalRef(env, local_cls_string);
// 刪除區域性引用
(*env)->DeleteLocalRef(env, local_cls_string);
// 再次驗證全域性引用是否建立成功
if (cls_string == NULL) {
return NULL;
}
}
// ....
return jstr;
}
弱全域性引用
弱全域性引用使用NewGlobalWeakRef
建立,使用DeleteGlobalWeakRef
釋放。下面簡稱弱引用。與全域性引用類似,弱引用可以跨方法、執行緒使用。但與全域性引用很重要不同的一點是,弱引用不會阻止GC回收它引用的物件。在newString這個函式中,我們也可以使用弱引用來儲存String的Class引用,因為java.lang.String這個類是系統類,永遠不會被GC回收。當原生代碼中快取的引用不一定要阻止GC回收它所指向的物件時,弱引用就是一個最好的選擇。假設,一個本地方法mypkg.MyCls.f需要快取一個指向類mypkg.MyCls2的引用,如果在弱引用中快取的話,仍然允許mypkg.MyCls2這個類被unload,因為弱引用不會阻止GC回收所引用的物件。請看下面的程式碼段:
JNIEXPORT void JNICALL
Java_mypkg_MyCls_f(JNIEnv *env, jobject self)
{
static jclass myCls2 = NULL;
if (myCls2 == NULL)
{
jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2");
if (myCls2Local == NULL)
{
return; /* 沒有找到mypkg/MyCls2這個類 */
}
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL)
{
return; /* 記憶體溢位 */
}
}
... /* 使用myCls2的引用 */
}
我們假設MyCls和MyCls2有相同的生命週期(例如,他們可能被相同的類載入器載入),因為弱引用的存在,我們不必擔心MyCls和它所在的原生代碼在被使用時,MyCls2這個類出現先被unload,後來又會preload的情況。當然,如果真的發生這種情況時(MyCls和MyCls2此時的生命週期不同),我們在使用弱引用時,必須先檢查快取過的弱引用是指向活動的類物件,還是指向一個已經被GC給unload的類物件。下面馬上告訴你怎樣檢查弱引用是否活動,即引用的比較。
引用比較
給定兩個引用(不管是全域性、區域性還是弱全域性引用),我們只需要呼叫IsSameObject來判斷它們兩個是否指向相同的物件。例如:(*env)->IsSameObject(env, obj1, obj2)
,如果obj1和obj2指向相同的物件,則返回JNI_TRUE(或者1),否則返回JNI_FALSE(或者0)。有一個特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null物件。如果obj是一個區域性或全域性引用,使用(*env)->IsSameObject(env, obj, NULL)
或者 obj == NULL 來判斷obj是否指向一個null物件即可。但需要注意的是,IsSameObject用於弱全域性引用與NULL比較時,返回值的意義是不同於區域性引用和全域性引用的:
jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// ... 業務邏輯處理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);
在上面的IsSameObject呼叫中,如果g_obj_ref指向的引用已經被回收,會返回JNI_TRUE,如果wobj仍然指向一個活動物件,會返回JNI_FALSE。
釋放全域性引用
每一個JNI引用被建立時,除了它所指向的JVM中物件的引用需要佔用一定的記憶體空間外,引用本身也會消耗掉一個數量的記憶體空間。作為一個優秀的程式設計師,我們應該對程式在一個給定的時間段內使用的引用數量要十分小心。短時間內建立大量而沒有被立即回收的引用很可能就會導致記憶體溢位。
當我們的原生代碼不再需要一個全域性引用時,應該馬上呼叫DeleteGlobalRef來釋放它。如果不手動呼叫這個函式,即使這個物件已經沒用了,JVM也不會回收這個全域性引用所指向的物件。
同樣,當我們的原生代碼不再需要一個弱全域性引用時,也應該呼叫DeleteWeakGlobalRef來釋放它,如果不手動呼叫這個函式來釋放所指向的物件,JVM仍會回收弱引用所指向的物件,但弱引用本身在引用表中所佔的記憶體永遠也不會被回收。
管理引用的規則
前面對三種引用已做了一個全面的介紹,下面來總結一下引用的管理規則和使用時的一些注意事項,使用好引用的目的就是為了減少記憶體使用和物件被引用保持而不能釋放,造成記憶體浪費。所以在開發當中要特別小心!
通常情況下,有兩種原生代碼使用引用時要注意:
1、 直接實現Java層宣告的native函式的原生代碼
當編寫這類原生代碼時,要當心不要造成全域性引用和弱引用的累加,因為本地方法執行完畢後,這兩種引用不會被自動釋放。
2、被用在任何環境下的工具函式。例如:方法呼叫、屬性訪問和異常處理的工具函式等。
編寫工具函式的原生代碼時,要當心不要在函式的呼叫軌跡上遺漏任何的區域性引用,因為工具函式被呼叫的場合和次數是不確定的,一量被大量呼叫,就很有可能造成記憶體溢位。所以在編寫工具函式時,請遵守下面的規則:
1> 一個返回值為基本型別的工具函式被呼叫時,它決不能造成區域性、全域性、弱全域性引用被回收的累加
2> 當一個返回值為引用型別的工具函式被呼叫時,它除了返回的引用以外,它決不能造成其它區域性、全域性、弱引用的累加
對於工具函式來說,為了使用快取技術而建立一些全域性引用或者弱全域性引用是正常的。如果一個工具函式返回的是一個引用,我們應該寫好註釋詳細說明返回引用的型別,以便於使用者更好的管理它們。下面的程式碼中,頻繁地呼叫工具函式GetInfoString,我們需要知道GetInfoString返回引用的型別是什麼,以便於每次使用完成後呼叫相應的JNI函式來釋放掉它。
while (JNI_TRUE) {
jstring infoString = GetInfoString(info);
... /* 處理infoString */
??? /* 使用完成之後,呼叫DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪一個函式來釋放這個引用呢?*/
}
函式NewLocalRef有時被用來確保一個工具函式返回一個區域性引用。我們改造一下newString這個函式,演示一下這個函式的用法。下面的newString是把一個被頻繁呼叫的字串“CommonString”快取在了全域性引用裡:
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
{
static jstring result;
/* 使用wstrncmp函式比較兩個Unicode字串 */
if (wstrncmp("CommonString", chars, len) == 0)
{
/* 將"CommonString"這個字串快取到全域性引用中 */
static jstring cachedString = NULL;
if (cachedString == NULL)
{
/* 先建立"CommonString"這個字串 */
jstring cachedStringLocal = ...;
/* 然後將這個字串快取到全域性引用中 */
cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);
}
// 基於全域性引用建立一個局引用返回,也同樣會阻止GC回收所引用的這個物件,因為它們指向的是同一個物件
return (*env)->NewLocalRef(env, cachedString);
}
...
return result;
}
在管理區域性引用的生命週期中,Push/PopLocalFrame是非常方便且安全的。我們可以在本地函式的入口處呼叫PushLocalFrame,然後在出口處呼叫PopLocalFrame,這樣的話,在函式內任何位置建立的區域性引用都會被釋放。而且,這兩個函式是非常高效的,強烈建議使用它們。需要注意的是,如果在函式的入口處呼叫了PushLocalFrame,記住要在函式所有出口(有return語句出現的地方)都要呼叫PopLocalFrame。在下面的程式碼中,對PushLocalFrame的呼叫只有一次,但呼叫PopLocalFrame確有多次,當然你也可以使用goto語句來統一處理。
jobject f(JNIEnv *env, ...)
{
jobject result;
if ((*env)->PushLocalFrame(env, 10) < 0)
{
/* 呼叫PushLocalFrame獲取10個區域性引用失敗,不需要呼叫PopLocalFrame */
return NULL;
}
...
result = ...; // 建立區域性引用result
if (...)
{
/* 返回前先彈出棧頂的frame */
result = (*env)->PopLocalFrame(env, result);
return result;
}
...
result = (*env)->PopLocalFrame(env, result);
/* 正常返回 */
return result;
}
上面的程式碼同樣演示了函式PopLocalFrame的第二個引數的用法,區域性引用result一開始在PushLocalFrame建立在當前frame裡面,而把result傳入PopLocalFrame中時,PopLocalFrame在彈出當前的frame前,會由result生成一個新的區域性引用,再將這個新生成的區域性引用儲存在上一個frame當中。