Android JNI開發詳解(5)-引用篇

原文出處:http://www.ccbu.cc/index.php/android/android-jni-ref.html

在JNI規範中定義了三種引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。

1. 局部引用

1.1 局部引用

通過NewLocalRef和各種JNI接口創建(FindClass、NewObject、GetObjectClass和NewCharArray等)。會阻止GC回收所引用的對象,不在本地函數中跨函數使用,不能跨線程使用。函數返回後局部引用所引用的對象會被JVM自動釋放,或調用DeleteLocalRef主動釋放 。

jclass jString = env->FindClass("java/lang/String");
jcharArray charArray = env->NewCharArray(len);
jstring jstr = env->NewStringUTF("hello world");
jstring strlocalRef = env->NewLocalRef(jString);   // 通過NewLocalRef創建本地引用
...
DeleteLocalRef(env,strlocalRef);

局部引用也稱本地引用,通常是在函數中創建並使用。會阻止GC回收所引用的對象。比如,調用NewObject接口創建一個新的對象實例並返回一個對這個對象的局部引用。局部引用只有在創建它的本地方法返回前有效,*本地方法返回到Java層之後,如果Java層沒有對返回的局部引用使用的話*,局部引用就會被JVM自動釋放。你可能會爲了提高程序的性能,在函數中將局部引用存儲在靜態變量中緩存起來,供下次調用時使用。這種方式是錯誤的,因爲函數返回後局部引很可能馬上就會被釋放掉,靜態變量中存儲的就是一個被釋放後的內存地址,成了一個野針對,下次再使用的時候就會造成非法地址的訪問,使程序崩潰。

1.2 釋放局部引用

釋放一個局部引用有兩種方式,一個是本地方法執行完畢後JVM自動釋放,另外一個是自己調用DeleteLocalRef手動釋放。既然JVM會在函數返回後會自動釋放所有局部引用,爲什麼還需要手動釋放呢?大部分情況下,我們在實現一個本地方法時不必擔心局部引用的釋放問題,函數被調用完成後,JVM 會自動釋放函數中創建的所有局部引用。但在某些情況下,我們必須要手動調用DeleteLocalRef釋放,否則會引起運行奔潰。JNI會將創建的局部引用都存儲在一個局部引用表中,如果這個表超過了最大容量限制,就會造成局部引用表溢出,使程序崩潰。Android上的JNI局部引用表最大數量是512個。當我們在實現一個本地方法時,可能需要創建大量的局部引用,如果沒有及時釋放,就有可能導致JNI局部引用表的溢出,所以,在不需要局部引用時就立即調用DeleteLocalRef手動刪除。比如,在下面的代碼中,本地代碼遍歷一個特別大的字符串數組,每遍歷一個元素,都會創建一個局部引用,當對使用完這個元素的局部引用時,就應該馬上手動釋放它。

for (i = 0; i < len; i++) {
     jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
     ...
     (*env)->DeleteLocalRef(env, jstr); // 使用完成之後馬上釋放
}

另外,如果使用 AttachCurrentThread 附加原生線程,那麼在線程分離之前,您運行的代碼將絕不會自動釋放局部引用。您創建的任何局部引用都必須手動刪除。

1.3 局部有效性

傳遞給原生方法的每個參數,以及 JNI 函數返回的幾乎每個對象都屬於“局部引用”。這意味着,局部引用在當前線程中的當前原生方法運行期間有效。 在原生方法返回後,即使對象本身繼續存在,該引用也無效。 局部引用不能跨線程使用,只在創建它的線程有效。不要試圖在一個線程中創建局部引用並存儲到全局引用中,然後在另外一個線程中使用。

1.4 管理局部引用

JNI提供了一系列函數來管理局部引用的生命週期。這些函數包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI規範指出,任何實現JNI規範的JVM,必須確保每個本地函數至少可以創建16個局部引用(可以理解爲虛擬機默認支持創建16個局部引用)。實際經驗表明,這個數量已經滿足大多數不需要和JVM中內部對象有太多交互的本地方函數。如果需要創建更多的引用,可以通過調用EnsureLocalCapacity函數,確保在當前線程中創建指定數量的局部引用,如果創建成功則返回0,否則創建失敗,並拋出OutOfMemoryError異常。 在下面的代碼中,遍歷數組時會獲取每個元素的引用,使用完了之後不手動刪除,不考慮內存因素的情況下,它可以爲這種創建大量的局部引用提供足夠的空間。由於沒有及時刪除局部引用,因此在函數執行期間,會消耗更多的內存。

/*確保函數中能創建len個局部引用*/
if(env->EnsureLocalCapacity(len) != 0) {
    ... /*申請len個局部引用的內存空間失敗 OutOfMemoryError*/
    return;
}
for(i=0; i < len; i++) {
    jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
    // ... 使用jstr字符串
    /*這裏不用釋放在for循環中臨時創建的局部引用*/
}

另外,除了EnsureLocalCapacity函數可以擴充指定容量的局部引用數量外,我們也可以利用Push/PopLocalFrame函數對創建作用範圍層層嵌套的局部引用。例如,我們把上面那段處理字符串數組的代碼用Push/PopLocalFrame函數對重寫:

#define REF_MAX 16 /*最大局部引用數量*/
for (i = 0; i < len; i++) {
    if (env->PushLocalFrame(REF_MAX) != 0) {
        ... /*內存溢出*/
    }
    jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
    ... /* 使用jstr */
    env->PopLocalFrame(NULL);
}

PushLocalFrame爲當前函數中需要用到的局部引用創建了一個引用堆棧,(如果之前調用PushLocalFrame已經創建了Frame,在當前的本地引用棧中仍然是有效的)每遍歷一次調用env->GetObjectArrayElement(objArray, i);返回一個局部引用時,JVM會自動將該引用壓入當前局部引用棧中。而PopLocalFrame負責銷燬棧中所有的引用。這樣一來,Push/PopLocalFrame函數對提供了對局部引用生命週期更方便的管理,而不需要時刻關注獲取一個引用後,再調用DeleteLocalRef來釋放引用。在上面的例子中,如果在處理jstr的過程當中又創建了局部引用,則PopLocalFrame執行時,這些局部引用將全都會被銷燬。在調用PopLocalFrame銷燬當前frame中的所有引用前,如果第二個參數result不爲空,會由result生成一個新的局部引用,再把這個新生成的局部引用存儲在上一個frame中。請看下面的示例:

// 函數原型
jobject     (*PopLocalFrame)(JNIEnv*, jobject)

jstring myJstr;
for (i = 0; i < len; i++) {
    if (env->PushLocalFrame(REF_MAX) != 0) {
        ... /*內存溢出*/
    }
    jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
    ... /* 使用jstr */
    if (i == 2) {
       myJstr = jstr;
    }
    myJstr = env->PopLocalFrame(myJstr);  // 銷燬局部引用棧前返回指定的引用
}

2.全局引用

**全局引用:**調用NewGlobalRef基於局部引用創建,會阻GC回收所引用的對象。可以跨方法、跨線程使用。JVM不會自動釋放,全局引用必須調用DeleteGlobalRef手動釋放env->DeleteGlobalRef(jString)。在調用 DeleteGlobalRef 之前,全局引用保證有效。

static jclass gString;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass jString = env->FindClass("java/lang/String");
    gString = env->NewGlobalRef(jString);
}

請注意,jfieldIDjmethodID 屬於不透明類型,不是對象引用,且不應傳遞給 NewGlobalRef。函數返回的 GetStringUTFCharsGetByteArrayElements 等原始數據指針也不屬於對象。(這些指針可以在線程之間傳遞,並且在匹配的 Release 調用完成之前一直有效。)

3.弱全局引用

**弱全局引用:**調用NewWeakGlobalRef基於局部引用或全局引用創建,不會阻止GC回收所引用的對象,可以跨方法、跨線程使用。引用不會自動釋放,在JVM認爲應該回收它的時候(比如內存緊張的時候)進行回收而被釋放。或調用DeleteWeakGlobalRef手動釋放。env->DeleteWeakGlobalRef(globalClass)

static jclass globalClass;
void Test(JNIEnv* env, jobject obj) {
    jclass localClass = env->FindClass("java/lang/String");
    globalClass = env->NewWeakGlobalRef(localClass);
}

與全局引用類似,弱引用可以跨方法、線程使用。但與全局引用很重要不同的一點是,弱引用不會阻止GC回收它引用的對象。當本地代碼中緩存的引用不一定要阻止GC回收它所指向的對象時,弱引用就是一個最好的選擇。所以在使用弱引用時,必須先檢查緩存過的弱引用是指向活動的類對象。 弱全局引用通過env->IsSameObject(obj1, obj2)來判斷指向的類對象是否爲活動的類對象。

4. 引用比較

給定兩個引用(不管是全局、局部還是弱全局引用),我們只需要調用IsSameObject來判斷它們兩個是否指向相同的對象。例如:env->IsSameObject(obj1, obj2),如果obj1和obj2指向相同的對象,則返回JNI_TRUE(或者1),否則返回JNI_FALSE(或者0)。有一個特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null對象。如果obj是一個局部或全局引用,使用env->IsSameObject(obj, NULL) 或者 obj == NULL 來判斷obj是否指向一個null對象即可。

jobject localRef = env->NewObject(xxx_cls,xxx_mid);
jobject wgRef = env->NewWeakGlobalRef(localRef);
...
jboolean isEqual = env->IsSameObject(wgRef, NULL);

在上面的IsSameObject調用中,如果g_obj_ref指向的引用已經被回收,會返回JNI_TRUE,如果g_obj_ref仍然指向一個活動對象,會返回JNI_FALSE

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章