JNI/NDK學(xué)習(xí)筆記(三)

*** 說明:本文不代表博主觀點(diǎn),均是由以下資料整理的讀書筆記。 ***

【參考資料】

1、向您的Android Studio項(xiàng)目添加C/C++代碼
2、Google開發(fā)者文檔 -- 添加C++代碼到現(xiàn)有Android Studio項(xiàng)目中
3、JNI Tips 英文原版
4、JNI Tips 中文
5、極客學(xué)院 JNI/NDK 開發(fā)指南
6、極客學(xué)院 深入理解 JNI
7、使用CMake構(gòu)建JNI環(huán)境
8、使用C和C++的區(qū)別
9、Google官方 NDK 文檔
10、極客學(xué)院 NDK開發(fā)課程
11、ndk-build 構(gòu)建 JNI 環(huán)境
12、開發(fā)自己的NDK程序
13、JNI/NDK開發(fā)教程
14、JNI層修改參數(shù)值
15、JNI引用和垃圾回收
16、《Android高級(jí)進(jìn)階》-- 顧浩鑫
17、《Android C++ 高級(jí)編程 -- 使用 NDK》 -- Onur Cinar


九、JNI 調(diào)用構(gòu)造方法和父類實(shí)例方法

這一節(jié)會(huì)詳細(xì)介紹初始一個(gè)對(duì)象的兩種方式,以及如何調(diào)用子類對(duì)象重寫的父類實(shí)例方法。下面通過一個(gè)示例來(lái)了解在 JNI 中是如何調(diào)用對(duì)象構(gòu)造方法和父類實(shí)例方法的。為了讓示例能清晰的體現(xiàn)構(gòu)造方法和父類實(shí)例方法的調(diào)用流程,定義了 Animal 和 Cat 兩個(gè)類,Animal 定義了一個(gè) String 形參的構(gòu)造方法,一個(gè)成員變量 name、兩個(gè)成員函數(shù) run 和 getName,Cat 繼承自 Animal,并重寫了 run 方法。在 JNI 中實(shí)現(xiàn)創(chuàng)建 Cat 對(duì)象的實(shí)例,調(diào)用 Animal 類的 run 和 getName 方法。代碼如下所示:

** Java代碼:**

public class Animal {

    protected String name;

    public Animal(String name) {
        this.name = name;
        Log.i("Miomin", "Animal Construct call...");
    }

    public String getName() {
        Log.i("Miomin", "Animal.getName Call...");
        return this.name;
    }

    public void run() {
        Log.i("Miomin", "Animal.run...");
    }
}

public class Cat extends Animal {

    public Cat(String name) {
        super(name);
        Log.i("Miomin", "Cat Construct call....");
    }

    @Override
    public String getName() {
        return "My name is " + this.name;
    }

    @Override
    public void run() {
        Log.i("Miomin", name + " Cat.run...");
    }
}

Native代碼:

extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_callSuperInstanceMethod(
        JNIEnv *env,
        jclass cls) {

    jclass cls_cat;
    jclass cls_animal;
    jmethodID mid_cat_init;
    jmethodID mid_run;
    jmethodID mid_getName;
    jstring c_str_name;
    jobject obj_cat;
    const char *name = NULL;

    // 1、獲取Cat類的class引用
    cls_cat = env->FindClass("com/scu/miomin/learncmake/Cat");
    if (cls_cat == NULL) {
        return;
    }

    // 2、獲取Cat的構(gòu)造方法ID(構(gòu)造方法的名統(tǒng)一為:<init>)
    mid_cat_init = env->GetMethodID(cls_cat, "<init>", "(Ljava/lang/String;)V");
    if (mid_cat_init == NULL) {
        return; // 沒有找到只有一個(gè)參數(shù)為String的構(gòu)造方法
    }

    // 3、創(chuàng)建一個(gè)String對(duì)象,作為構(gòu)造方法的參數(shù)
    c_str_name = env->NewStringUTF("湯姆貓");
    if (c_str_name == NULL) {
        return; // 創(chuàng)建字符串失敗(內(nèi)存不夠)
    }

    //  4、創(chuàng)建Cat對(duì)象的實(shí)例(調(diào)用對(duì)象的構(gòu)造方法并初始化對(duì)象)
    obj_cat = env->NewObject(cls_cat, mid_cat_init, c_str_name);
    if (obj_cat == NULL) {
        return;
    }

    //-------------- 5、調(diào)用Cat父類Animal的run和getName方法 --------------
    cls_animal = env->FindClass("com/scu/miomin/learncmake/Animal");
    if (cls_animal == NULL) {
        return;
    }


    /**
     * 例1: 調(diào)用父類的run方法
     */
    mid_run = env->GetMethodID(cls_animal, "run", "()V");    // 獲取父類Animal中run方法的id
    if (mid_run == NULL) {
        return;
    }

    // 注意:obj_cat是Cat的實(shí)例,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID
    env->CallNonvirtualVoidMethod(obj_cat, cls_animal, mid_run);


    /**
     * 例2:調(diào)用父類的getName方法
     */
    // 獲取父類Animal中g(shù)etName方法的id
    mid_getName = env->GetMethodID(cls_animal, "getName", "()Ljava/lang/String;");
    if (mid_getName == NULL) {
        return;
    }

    c_str_name = (jstring) env->CallNonvirtualObjectMethod(obj_cat, cls_animal, mid_getName);
    name = env->GetStringUTFChars(c_str_name, NULL);
    LOGV("In C: Animal Name is %s\n", name);

    // 釋放從java層獲取到的字符串所分配的內(nèi)存
    env->ReleaseStringUTFChars(c_str_name, name);

    quit:
    // 刪除局部引用(jobject或jobject的子類才屬于引用變量),允許VM釋放被局部變量所引用的資源
    env->DeleteLocalRef(cls_cat);
    env->DeleteLocalRef(cls_animal);
    env->DeleteLocalRef(c_str_name);
    env->DeleteLocalRef(obj_cat);
}

如果一個(gè)方法被定義在父類中,在子類中被覆蓋,也可以調(diào)用父類中的這個(gè)實(shí)例方法。JNI 提供了一系列函數(shù)CallNonvirtualXXXMethod 來(lái)支持調(diào)用各種返回值類型的實(shí)例方法。其實(shí)在開發(fā)當(dāng)中,這種調(diào)用父類實(shí)例方法的情況是很少遇到的,通常在 JAVA 中可以很簡(jiǎn)單地做到: super.func()。


十、JNI 調(diào)用性能測(cè)試及優(yōu)化

在 C/C++ 中寫的程序可以避開 JVM 的內(nèi)存開銷過大的限制、處理高性能的計(jì)算、調(diào)用系統(tǒng)服務(wù)等功能。

10.1 Java 調(diào)用 JNI 空函數(shù)與 Java 調(diào)用 Java 空方法性能

Java 程序是運(yùn)行在 JVM 上的,所以在 Java 中調(diào)用 C/C++ 或其它語(yǔ)言這種跨語(yǔ)言的接口時(shí),或者說在 C/C++ 代碼中通過 JNI 接口訪問 Java 中對(duì)象的方法或?qū)傩詴r(shí),相比 Java 調(diào)用自已的方法,性能是非常低的!JDK 版本越高,JNI 調(diào)用的性能也越好。在 JDK1.5 中,僅僅是空方法調(diào)用,JNI 的性能就要比 Java 內(nèi)部調(diào)用慢將近 5 倍,而在 JDK1.4 下更是慢了十多倍。

10.2 JNI查找方法ID、字段ID、Class引用性能

當(dāng)我們?cè)?Native 代碼中要訪問 Java 對(duì)象的字段或調(diào)用它們的方法時(shí),Native 代碼必須調(diào)用 FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID()和 GetStaticMethodID()。對(duì)于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),為特定類返回的 ID 不會(huì)在 JVM 進(jìn)程的生存期內(nèi)發(fā)生變化。但是,獲取字段或方法的調(diào)用有時(shí)會(huì)需要在 JVM 中完成大量工作,因?yàn)樽侄魏头椒赡苁菑某愔欣^承而來(lái)的,這會(huì)讓 JVM 向上遍歷類層次結(jié)構(gòu)來(lái)找到它們。由于 ID 對(duì)于特定類是相同的,因此只需要查找一次,然后便可重復(fù)使用。同樣,查找類對(duì)象的開銷也很大,因此也應(yīng)該緩存它們。

消耗時(shí)間最多的就是查找class,因此在 native 里保存 class 和 member id 是很有必要的。class 和 member id 在一定范圍內(nèi)是穩(wěn)定的,但在動(dòng)態(tài)加載的 class loader 下,保存全局的 class 要么可能失效,要么可能造成無(wú)法卸載classloader,在諸如 OSGI 框架下的 JNI 應(yīng)用還要特別注意這方面的問題。

兩種緩存方式參考 :

http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/performance.html

http://www.2cto.com/kf/201501/368946.html


十一、JNI 局部引用、全局引用和弱全局引用

引用是在 JNI 中最容易出錯(cuò)的一個(gè)點(diǎn),如果使用不當(dāng),容易使程序造成內(nèi)存溢出,程序崩潰等現(xiàn)象。

在 Java 中內(nèi)存管理是完全透明的,內(nèi)存分配和釋放交給 GC 就可以了,C/C++ 則需要自己控制內(nèi)存的分配和釋放;但是凡事有利也有弊,比如在做 Android 開發(fā)的時(shí)候,內(nèi)存使用就受虛擬機(jī)的限制,從最初版本的16~24M,到后來(lái)的 32M 到 64M。但在 C/C++ 這層,就完全不受虛擬機(jī)的限制了。比如要在 Android 中要存儲(chǔ)一張超高清的圖片,剛好這張圖片的大小超過了 Dalivk 虛擬機(jī)對(duì)每個(gè)應(yīng)用的內(nèi)存大小限制,Java 此時(shí)就顯得無(wú)能為力了,但在C/C++ 看來(lái)就是小菜一碟了,malloc(1024102450)。

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

創(chuàng)建局部引用的本地方法返回后(注意:這里是指返回到j(luò)ava方法),局部引用將變成無(wú)效。而全局引用以及弱全局引用在本地方法返回后,仍然有效。垃圾回收器無(wú)法回收本地方法創(chuàng)建的局部引用和全局引用,但可以回收本地方法創(chuàng)建的弱全局引用。

11.1 局部引用

(1)局部引用

通過 NewLocalRef 和各種 JNI 接口創(chuàng)建(FindClass、NewObject、GetObjectClass和NewCharArray等)。會(huì)阻止 GC 回收所引用的對(duì)象,不在本地函數(shù)中跨函數(shù)使用,不能跨線程使用。Native 方法返回到 Java 層之后,如果 Java 層沒有對(duì)返回的局部引用使用的話,局部引用就會(huì)被 JVM 自動(dòng)釋放,或調(diào)用 DeleteLocalRef 釋放。(當(dāng)Native函數(shù)執(zhí)行完成之后,如果局部引用沒有被Native代碼顯式刪除,那么局部引用在Java虛擬機(jī)中還是有效的。Java虛擬機(jī)來(lái)決定在什么時(shí)候來(lái)刪除這個(gè)對(duì)象,而且直到JAVA層沒有對(duì)它的引用,可以通過Native函數(shù)返回而把它引用到JAVA層,它才能被JVM回收并釋放。)

局部引用不能作為靜態(tài)變量,也不能作為全局變量。

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函數(shù)創(chuàng)建

(2)錯(cuò)誤的引用緩存

你可能會(huì)為了提高程序的性能,在函數(shù)中將局部引用存儲(chǔ)在靜態(tài)變量中緩存起來(lái),供下次調(diào)用時(shí)使用。這種方式是錯(cuò)誤的,因?yàn)楹瘮?shù)返回后局部引很可能馬上就會(huì)被釋放掉,靜態(tài)變量中存儲(chǔ)的就是一個(gè)被釋放后的內(nèi)存地址,成了一個(gè)野針對(duì),下次再使用的時(shí)候就會(huì)造成非法地址的訪問,使程序崩潰。

(3)釋放局部引用

JNI 會(huì)將創(chuàng)建的局部引用都存儲(chǔ)在一個(gè)局部引用表中,如果這個(gè)表超過了最大容量限制,就會(huì)造成局部引用表溢出,使程序崩潰。Android 上的 JNI 局部引用表最大數(shù)量是 512 個(gè)。當(dāng)我們?cè)趯?shí)現(xiàn)一個(gè)本地方法時(shí),可能需要?jiǎng)?chuàng)建大量的局部引用,如果沒有及時(shí)釋放,就有可能導(dǎo)致 JNI 局部引用表的溢出,所以,在不需要局部引用時(shí)就立即調(diào)用 DeleteLocalRef 手動(dòng)刪除。比如,在下面的代碼中,本地代碼遍歷一個(gè)特別大的字符串?dāng)?shù)組,每遍歷一個(gè)元素,都會(huì)創(chuàng)建一個(gè)局部引用,當(dāng)對(duì)使用完這個(gè)元素的局部引用時(shí),就應(yīng)該馬上手動(dòng)釋放它。

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

/* 假如這是一個(gè)本地方法實(shí)現(xiàn) */
JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
   lref = ...              /* lref引用的是一個(gè)大的Java對(duì)象 */
   ...                     /* 在這里已經(jīng)處理完業(yè)務(wù)邏輯后,這個(gè)對(duì)象已經(jīng)使用完了 */
   (*env)->DeleteLocalRef(env, lref); /* 及時(shí)刪除這個(gè)對(duì)這個(gè)大對(duì)象的引用,GC就可以對(duì)它回收,并釋放相應(yīng)的資源*/
   lengthyComputation();   /* 在里有個(gè)比較耗時(shí)的計(jì)算過程 */
   return;                 /* 計(jì)算完成之后,函數(shù)返回之前所有引用都已經(jīng)釋放 */
}

局部引用在Native代碼顯示釋放非常重要。你可能會(huì)問,既然Java虛擬機(jī)會(huì)自動(dòng)釋放局部變量為什么還需要我在Native代碼中顯示釋放呢?原因有以下幾點(diǎn):

  • 1、Java虛擬機(jī)默認(rèn)為Native引用分配的局部引用數(shù)量是有限的,大部分的Java虛擬機(jī)實(shí)現(xiàn)默認(rèn)分配16個(gè)局部引用。當(dāng)然Java虛擬機(jī)也提供API(PushLocalFrame,EnsureLocalCapacity)讓你申請(qǐng)更多的局部引用數(shù)量(Java虛擬機(jī)不保證你一定能申請(qǐng)到)。有限的資源當(dāng)然要省著點(diǎn)用,否則將會(huì)被Java虛擬機(jī)無(wú)情拋棄(程序崩潰)。JNI編程中,實(shí)現(xiàn)Native代碼時(shí)強(qiáng)烈建議調(diào)用PushLocalFrame,EnsureLocalCapacity來(lái)確保Java虛擬機(jī)為你準(zhǔn)備好了局部變量空間。

  • 2、如果你實(shí)現(xiàn)的Native函數(shù)是工具函數(shù),會(huì)被頻繁的調(diào)用。如果你在Native函數(shù)中沒有顯示刪除局部引用,那么每次調(diào)用該函數(shù)Java虛擬機(jī)都會(huì)創(chuàng)建一個(gè)新的局部引用,造成局部引用過多。尤其是該函數(shù)在Native代碼中被頻繁調(diào)用,代碼的控制權(quán)沒有交還給Java虛擬機(jī),所以Java虛擬機(jī)根本沒有機(jī)會(huì)釋放這些局部變量。退一步講,就算該函數(shù)直接返回給Java虛擬機(jī),也不能保證沒有問題,我們不能假設(shè)Native函數(shù)返回Java虛擬機(jī)之后,Java虛擬機(jī)馬上就會(huì)回收Native函數(shù)中創(chuàng)建的局部引用,依賴于Java虛擬機(jī)實(shí)現(xiàn)。所以我們?cè)趯?shí)現(xiàn)Native函數(shù)時(shí)一定要記著刪除不必要的局部引用,否則你的程序就有潛在的風(fēng)險(xiǎn),不知道什么時(shí)候就會(huì)爆發(fā)。

  • 3、如果你Native函數(shù)根本就不返回。比如消息循環(huán)函數(shù)——死循環(huán)等待消息,處理消息。如果你不顯示刪除局部引用,很快將會(huì)造成Java虛擬機(jī)的局部引用內(nèi)存溢出。

這里介紹一下PushLocalFrame和PopLocalFrame函數(shù)。這兩個(gè)函數(shù)是成對(duì)使用的,先調(diào)用PushLocalFrame,然后創(chuàng)建局部引用,并對(duì)其進(jìn)行處理,最后調(diào)用PushLocalFrame釋放局部引用,這時(shí)Java虛擬機(jī)也可以對(duì)其指向的對(duì)象進(jìn)行垃圾回收??梢杂肅語(yǔ)言的棧來(lái)理解這對(duì)JNI API,調(diào)用PushLocalFrame之后Native代碼創(chuàng)建的所有局部引用全部入棧,當(dāng)調(diào)用PopLocalFrame之后,入棧的局部引用除了需要返回的局部引用(PushLocalFrame和PopLocalFrame這對(duì)函數(shù)可以返回一個(gè)局部引用給外部)之外,全部出棧,Java虛擬機(jī)這時(shí)可以釋放他們指向的對(duì)象。具體的用法可以參考手冊(cè)。這兩個(gè)函數(shù)使JNI的局部引用由于和C語(yǔ)言的局部變量用法類似,所以強(qiáng)烈推薦使用。

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

PS:還要注意的一個(gè)問題是,局部引用不能跨線程使用,只在創(chuàng)建它的線程有效。不要試圖在一個(gè)線程中創(chuàng)建局部引用并存儲(chǔ)到全局引用中,然后在另外一個(gè)線程中使用。

11.2 全局引用

調(diào)用 NewGlobalRef 基于局部引用創(chuàng)建,會(huì)阻 GC 回收所引用的對(duì)象??梢钥绶椒?、跨線程使用。JVM 不會(huì)自動(dòng)釋放,必須調(diào)用 DeleteGlobalRef 手動(dòng)釋放。請(qǐng)注意NewGlobalRef的第二個(gè)參數(shù),既可以用一個(gè)局部引用,也可以用全局引用生成一個(gè)全局引用,當(dāng)然也可以用弱全局引用生成一個(gè)全局引用,但是這中情況有特殊的用途,后文會(huì)介紹。

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);
}

全局引用可以跨方法、跨線程使用,直到它被手動(dòng)釋放才會(huì)失效。同局部引用一樣,也會(huì)阻止它所引用的對(duì)象被 GC 回收。與局部引用創(chuàng)建方式不同的是,只能通過 NewGlobalRef 函數(shù)創(chuàng)建。下面這個(gè)版本的 newString 演示怎么樣使用一個(gè)全局引用。

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引用緩存到全局引用當(dāng)中
        cls_string = (*env)->NewGlobalRef(env, local_cls_string);

        // 刪除局部引用
        (*env)->DeleteLocalRef(env, local_cls_string);

        // 再次驗(yàn)證全局引用是否創(chuàng)建成功
        if (cls_string == NULL) {
            return NULL;
        }
    }

    // ....
    return jstr;
}

11.3 弱全局引用

調(diào)用 NewWeakGlobalRef 基于局部引用或全局引用創(chuàng)建,不會(huì)阻止 GC 回收所引用的對(duì)象,可以跨方法、跨線程使用。引用不會(huì)自動(dòng)釋放,在 JVM 認(rèn)為應(yīng)該回收它的時(shí)候(比如內(nèi)存緊張的時(shí)候)進(jìn)行回收而被釋放?;蛘{(diào)用 DeleteWeakGlobalRef 手動(dòng)釋放。

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);
}

所有的JNI方法都接收局部引用和全局引用作為參數(shù)。相同對(duì)象的引用卻可能具有不同的值。例如,用相同對(duì)象連續(xù)地調(diào)用NewGlobalRef得到返回值可能是不同的。為了檢查兩個(gè)引用是否指向的是同一個(gè)對(duì)象,你必須使用IsSameObject函數(shù)。絕不要在本地代碼中用==符號(hào)來(lái)比較兩個(gè)引用。

得出的結(jié)論就是你絕不要在本地代碼中假定對(duì)象的引用是常量或者是唯一的。代表一個(gè)對(duì)象的32位值從方法的一次調(diào)用到下一次調(diào)用可能有不同的值。在連續(xù)的調(diào)用過程中兩個(gè)不同的對(duì)象卻可能擁有相同的32位值。不要使用jobject的值作為key.

JNI API提供了引用比較函數(shù)IsSameObject,用弱全局引用和NULL進(jìn)行比較,如果返回JNI_TRUE,則說明弱全局引用指向的對(duì)象已經(jīng)被釋放。需要重新初始化弱全局引用。應(yīng)該這樣寫:

static jobject weak_global_ref = NULL;

if((*env)->IsSameObject(env, weak_global_ref, NULL) == JNI_TRUE) {
  weak_global_ref = NewWeakGlobalRef(...);
}
static jobject weak_global_ref = NULL;
jobject local_ref;

/* We ensure create local_ref success */
while ( week_global_ref == NULL || (local_ref = NewLocalRef(env, weak_global_ref)) == NULL ) {
 /* Init week global referrence again */
 weak_global_ref = NewWeakGlobalRef(...);
}

/* Process local_ref */

(*env)->DeleteLocalRef(env, local_ref);

11.4 引用比較

給定兩個(gè)引用(不管是全局、局部還是弱全局引用),我們只需要調(diào)用 IsSameObject 來(lái)判斷它們兩個(gè)是否指向相同的對(duì)象。例如:env->IsSameObject(obj1, obj2),如果 obj1 和 obj2 指向相同的對(duì)象,則返回 JNI_TRUE(或者 1),否則返回 JNI_FALSE(或者 0)。

11.5 引用的釋放

每一個(gè) JNI 引用被建立時(shí),除了它所指向的 JVM 中對(duì)象的引用需要占用一定的內(nèi)存空間外,引用本身也會(huì)消耗掉一個(gè)數(shù)量的內(nèi)存空間。作為一個(gè)優(yōu)秀的程序員,我們應(yīng)該對(duì)程序在一個(gè)給定的時(shí)間段內(nèi)使用的引用數(shù)量要十分小心。短時(shí)間內(nèi)創(chuàng)建大量而沒有被立即回收的引用很可能就會(huì)導(dǎo)致內(nèi)存溢出。 ??? 當(dāng)我們的本地代碼不再需要一個(gè)全局引用時(shí),應(yīng)該馬上調(diào)用 DeleteGlobalRef 來(lái)釋放它。如果不手動(dòng)調(diào)用這個(gè)函數(shù),即使這個(gè)對(duì)象已經(jīng)沒用了,JVM 也不會(huì)回收這個(gè)全局引用所指向的對(duì)象。

同樣,當(dāng)我們的本地代碼不再需要一個(gè)弱全局引用時(shí),也應(yīng)該調(diào)用 DeleteWeakGlobalRef 來(lái)釋放它,如果不手動(dòng)調(diào)用這個(gè)函數(shù)來(lái)釋放所指向的對(duì)象,JVM 仍會(huì)回收弱引用所指向的對(duì)象,但弱引用本身在引用表中所占的內(nèi)存永遠(yuǎn)也不會(huì)被回收。

注意:弱全局引用是可以用來(lái)緩存jclass對(duì)象,但是用全局引用來(lái)緩存jclass對(duì)象將非常的危險(xiǎn)。這里需要簡(jiǎn)單介紹一下Native的共享庫(kù)的卸載。當(dāng)Class Loader釋放完所有的class后,然后Class Loader會(huì)卸載Native的共享庫(kù)。如果我們用全局引用來(lái)緩存jclass對(duì)象的話,根據(jù)前面對(duì)全局引用對(duì)Java虛擬機(jī)垃圾回收機(jī)制的影響,將會(huì)阻止Java虛擬機(jī)回收該對(duì)象。如果我們不顯式的釋放全局引用(通過DeleteGlobalRef),則Class Loader也將不能釋放這個(gè)jclass對(duì)象,進(jìn)而造成Class Loader不能卸載Native的共享庫(kù)(永遠(yuǎn)無(wú)法釋放)。如果用弱全局引用來(lái)緩存將不會(huì)有這個(gè)問題,Java虛擬機(jī)隨時(shí)都可以釋放它指向的對(duì)象。

還有一種不常見的情況值得一提,如果你使用AttachCurrentThread連接(attach)了本地進(jìn)程,正在運(yùn)行的代碼在線程分離(detach)之前決不會(huì)自動(dòng)釋放局部引用。你創(chuàng)建的任何局部引用必須手動(dòng)刪除。通常,任何在循環(huán)中創(chuàng)建局部引用的本地代碼可能都需要做一些手動(dòng)刪除。

11.6 總結(jié)

  • 1、局部引用是Native代碼中最常用的引用。大部分局部引用都是通過JNI API返回來(lái)創(chuàng)建,也可以通過調(diào)用NewLocalRef來(lái)創(chuàng)建。另外強(qiáng)烈建議Native函數(shù)返回值為局部引用。局部引用只在當(dāng)前調(diào)用上下文中有效,所以局部引用不能用Native代碼中的靜態(tài)變量和全局變量來(lái)保存。另外時(shí)刻要記著Java虛擬機(jī)局部引用的個(gè)數(shù)是有限的,編程的時(shí)候強(qiáng)烈建議調(diào)用EnsureLocalCapacity,PushLocalFrame和PopLocalFrame來(lái)確保Native代碼能夠獲得足夠的局部引用數(shù)量。

  • 2、全局變量必須要通過NewGlobalRef創(chuàng)建,通過DeleteGlobalRef刪除。主要用來(lái)緩存Field ID和Method ID。全局引用可以在多線程之間共享其指向的對(duì)象。在C語(yǔ)言中以靜態(tài)變量和全局變量來(lái)保存。

  • 3、全局引用和局部引用可以阻止Java虛擬機(jī)回收其指向的對(duì)象。

  • 4、弱全局引用必須要通過NewWeakGlobalRef創(chuàng)建,通過DeleteWeakGlobalRef銷毀??梢栽诙嗑€程之間共享其指向的對(duì)象。在C語(yǔ)言中通過靜態(tài)變量和全局變量來(lái)保持弱全局引用。弱全局引用指向的對(duì)象隨時(shí)都可能會(huì)被Java虛擬機(jī)回收,所以使用的時(shí)候需要時(shí)刻注意檢查其有效性。弱全局引用經(jīng)常用來(lái)緩存jclass對(duì)象。

  • 5、全局引用和弱全局引用可以在多線程中共享其指向?qū)ο?,但是在多線程編程中需要注意多線程同步。強(qiáng)烈建議在JNI_OnLoad初始化全局引用和弱全局引用,然后在多線程中進(jìn)行讀全局引用和弱全局引用,這樣不需要對(duì)全局引用和弱全局引用同步(只有讀操作不會(huì)出現(xiàn)不一致情況)。


十二、JNI 異常

JNI的錯(cuò)誤檢查很少。錯(cuò)誤發(fā)生時(shí)通常會(huì)導(dǎo)致崩潰。Android也提供了一種模式,叫做CheckJNI,這當(dāng)中JavaVM和JNIEnv函數(shù)表指針被換成了函數(shù)表,它在調(diào)用標(biāo)準(zhǔn)實(shí)現(xiàn)之前執(zhí)行了一系列擴(kuò)展檢查的:

  • 數(shù)組:試圖分配一個(gè)長(zhǎng)度為負(fù)的數(shù)組。
  • 壞指針:傳入一個(gè)不完整jarray/jclass/jobject/jstring對(duì)象到JNI函數(shù),或者調(diào)用JNI函數(shù)時(shí)使用空指針傳入到一個(gè)不能為空的參數(shù)中去。
  • 類名:傳入了除“java/lang/String”之外的類名到JNI函數(shù)。
    關(guān)鍵調(diào)用:在一個(gè)“關(guān)鍵的(critical)”get和它對(duì)應(yīng)的release之間做出JNI調(diào)用。
  • 直接的ByteBuffers:傳入不正確的參數(shù)到NewDirectByteBuffer。
    異常:當(dāng)一個(gè)異常發(fā)生時(shí)調(diào)用了JNI函數(shù)。
  • JNIEnvs:在錯(cuò)誤的線程中使用一個(gè)JNIEnv。
  • jfieldIDs:使用一個(gè)空jfieldID,或者使用jfieldID設(shè)置了一個(gè)錯(cuò)誤類型的值到字段(比如說,試圖將一個(gè)StringBuilder賦給String類型的域),或者使用一個(gè)靜態(tài)字段下的jfieldID設(shè)置到一個(gè)實(shí)例的字段(instance field)反之亦然,或者使用的一個(gè)類的jfieldID卻來(lái)自另一個(gè)類的實(shí)例。
  • jmethodIDs:當(dāng)調(diào)用Call*Method函數(shù)時(shí)時(shí)使用了類型錯(cuò)誤的
  • jmethodID:不正確的返回值,靜態(tài)/非靜態(tài)的不匹配,this的類型錯(cuò)誤(對(duì)于非靜態(tài)調(diào)用)或者錯(cuò)誤的類(對(duì)于靜態(tài)類調(diào)用)。
    引用:在類型錯(cuò)誤的引用上使用了DeleteGlobalRef/DeleteLocalRef。
  • 釋放模式:調(diào)用release使用一個(gè)不正確的釋放模式(其它非 0,JNI_ABORT,JNI_COMMIT的值)。
  • 類型安全:從你的本地代碼中返回了一個(gè)不兼容的類型(比如說,從一個(gè)聲明返回String的方法卻返回了StringBuilder)。
  • UTF-8:傳入一個(gè)無(wú)效的變形UTF-8字節(jié)序列到JNI調(diào)用。

如果你正在使用模擬器,CheckJNI默認(rèn)是打開的。

如果你有一臺(tái)root過的設(shè)備,你可以使用下面的命令序列來(lái)重啟運(yùn)行時(shí)(runtime),啟用CheckJNI。

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

隨便哪一種,當(dāng)運(yùn)行時(shí)(runtime)啟動(dòng)時(shí)你將會(huì)在你的日志輸出中見到如下的字符:

D AndroidRuntime: CheckJNI is ON

如果你有一臺(tái)常規(guī)的設(shè)備,你可以使用下面的命令:

adb shell setprop debug.checkjni 1

這將不會(huì)影響已經(jīng)在運(yùn)行的app,但是從那以后啟動(dòng)的任何app都將打開CheckJNI(改變屬性為其它值或者只是重啟都將會(huì)再次關(guān)閉CheckJNI)。這種情況下,你將會(huì)在下一次app啟動(dòng)時(shí),在日志輸出中看到如下字符:

D Late-enabling CheckJNI
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容