Android-JNI開發(fā)系列《五》局部引用&全局引用&全局弱引用

人間觀察

好像什么都來得及,又好像什么都來不及。

本篇文章主要介紹在jni開發(fā)中常見的三種引用的使用方法和注意事項以及jni和java交互的緩存策略。

我們知道Java是一門純面象對象的語言,除了基本數(shù)據(jù)類型外,其它任何類型所創(chuàng)建的對象的內(nèi)存都存在堆空間中。內(nèi)存由JVM 的GC(Garbage Collection)垃圾回收進行管理。

但是對于c,c++中以及用c/c++編寫的jni來說同樣需要手動管理和處理內(nèi)存,特別是引用類型的對象。malloc,realloc,free ,delete ,不像java有jvm對每個進程內(nèi)存的限制,特別是Android移動端使用不當(dāng)給你oom好不啦。而在c++/c只要你想要多大的來隨便搞(只要系統(tǒng)內(nèi)存充足)但是需要釋放。各有各的優(yōu)勢。

在jni中分為局部引用,全局引用,全局弱引用,個人認為有點類似于java中
局部引用,強引用,軟引用SoftReference。在使用介紹之前我們先看一下jni中的基本類型和引用類型有哪些以及對應(yīng)關(guān)系。

jni數(shù)據(jù)類型

基本數(shù)據(jù)類型

java與Native映射關(guān)系如下表所示:

Java類型 Native 類型 Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void not applicable

引用數(shù)據(jù)類型

外面的為jni中的,括號中的java中的。

  • jobject
    • jclass (java.lang.Class objects)
    • jstring (java.lang.String objects)
    • jarray (arrays)
      • jobjectArray (object arrays)
      • jbooleanArray (boolean arrays)
      • jbyteArray (byte arrays)
      • jcharArray (char arrays)
      • jshortArray (short arrays)
      • jintArray (int arrays)
      • jlongArray (long arrays)
      • jfloatArray (float arrays)
      • jdoubleArray (double arrays)
  • jthrowable (java.lang.Throwable objects)

上面的層次中的jni的引用類型代表了繼承關(guān)系,jbooleanArray繼承jarray,jarray繼承jobject,最終都繼承jobject。

局部引用

通過調(diào)用jni的一些方法比如FindClass,NewCharArray,NewStringUTF等只要是返回上面介紹的jni的引用類型都屬于局部引用,局部引用的生命周期只在方法中效,不能垮線程跨方法使用,函數(shù)退出后局部引用所引用的對象會被JVM自動釋放,或顯示調(diào)用DeleteLocalRef釋放。局部引用的也可以通過(*env)->NewLocalRef(env,local_ref)方法創(chuàng)建,一般不常用。

如下示例:

// jni_ref.cpp
// 在jni中調(diào)用java String類構(gòu)造返回String
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jnilocalRef(JNIEnv *env, jobject instance) {

    // 局部引用
    jclass local_j_cls = env->FindClass("java/lang/String");

    // 調(diào)用public String(char[] value); 構(gòu)造方法。 為了演示更多的局部引用
    jmethodID j_mid = env->GetMethodID(local_j_cls, "<init>", "([C)V");
    // 局部引用
    jcharArray local_j_charArr = env->NewCharArray(8);
    // 局部引用
    jstring local_str = env->NewStringUTF("LocalRef");
    const jchar *j_char = env->GetStringChars(local_str, nullptr);

    env->SetCharArrayRegion(local_j_charArr, 0, 8, j_char);

    jstring j_str = (jstring) env->NewObject(local_j_cls, j_mid, local_j_charArr);

    // 釋放局部引用,也可以不用調(diào)用在方法結(jié)束后jvm會自動回收,最好有良好的編碼習(xí)慣
    env->DeleteLocalRef(local_j_cls);
    env->DeleteLocalRef(local_str);
    env->DeleteLocalRef(local_j_charArr);

    // 也可以通過NewLocalRef函數(shù)創(chuàng)建 (*env)->NewLocalRef(env,local_ref);這個方法一般很少用。
    // 函數(shù)返回后局部引用所引用的對象會被JVM自動釋放,或調(diào)用DeleteLocalRef釋放。(*env)->DeleteLocalRef(env,local_ref)

    // ReleaseStringChars和GetStringChars對應(yīng)
    env->ReleaseStringChars(j_str, j_char);

    return j_str;
}

例子中的local_j_cls,local_j_charArr,local_j_charArr,j_str 都是局部引用類型。最后調(diào)用了DeleteLocalRef來釋放。
有同學(xué)問了,既然局部引用不用手動釋放,可不可以不用調(diào)用DeleteLocalRef方法。
咦,你這個小可愛,好問題哦!

好問題哦.jpg

我網(wǎng)上搜索了下,大部分的文章說了下會有限制。超過512個局部引用(為什么是這個數(shù)字,一看就是一個有情懷的程序員)會造成局部引用表溢出。我還是想測試一下如下

// jni_ref.cpp
    LOG_D("localRefOverflow start");
    for (int i = 0; i < count; i++) {
        jclass local_j_cls = env->FindClass("java/util/ArrayList");
        // env->DeleteLocalRef(local_j_cls);
    }
    LOG_D("localRefOverflow end");

count =513,沒有報錯,打印了localRefOverflow end

count =2000,沒有報錯,打印了localRefOverflow end

count =10000,沒有報錯,打印了localRefOverflow end

count =10 0000,沒有報錯,打印了localRefOverflow end

count =100 0000,沒有報錯,打印了localRefOverflow end
...

我靠,WTF? 直接for循環(huán)900w次。異常出現(xiàn)了。

2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] JNI ERROR (app bug): local reference table overflow (max=8388608)
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] local reference table dump:
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]   Last 10 entries (of 8388608):
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388607: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388606: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388605: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388604: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388603: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388602: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388601: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388600: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388599: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388598: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]   Summary:
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388604 of java.lang.Class (3 unique instances)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]         3 of java.lang.String (3 unique instances)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]         1 of java.lang.String[] (3 elements)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]  Resizing failed: Requested size exceeds maximum: 16777216

8388608 ,可以猜測是不同的Android 版本導(dǎo)致,Android經(jīng)常這樣不同的API或者功能在不同的版本上表現(xiàn)不一樣。 而我我用的Android 8.1的系統(tǒng)。為什么512沒有報錯了。
Android 8.0 之前局部引用表的上限是512個引用,Android 8.0后局部引用表上限提升到了8388608個引用。大家想一探究竟的話可以在如下Android 底層代碼中看一下。

需要翻墻
有關(guān)底層源碼

看源碼的同時我們也看到了比如FindClass 等方法,在最后方法的最后都有類似添加到局部引用表里的代碼,也就是說需要我們手動刪除局部引用。

 static jclass FindClass(JNIEnv* env, const char* name) {
    CHECK_NON_NULL_ARGUMENT(name);
    Runtime* runtime = Runtime::Current();
    ClassLinker* class_linker = runtime->GetClassLinker();
    std::string descriptor(NormalizeJniClassDescriptor(name));
    ScopedObjectAccess soa(env);
    mirror::Class* c = nullptr;
    if (runtime->IsStarted()) {
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader(soa)));
      c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
    } else {
      c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
    }
    return soa.AddLocalReference<jclass>(c);
  }

綜上可以看出并不是局部引用不用調(diào)用DeleteLocalRef來釋放。而是建議調(diào)用一下。如果你的jni方法很簡單&與java交互很少也可以不調(diào)用。但是如下的一些情況需要手動顯示的調(diào)用,為了防止內(nèi)存溢出和局部引用表溢出。

  1. 如上我們模擬的情況,在for循環(huán)里或者其它操作類似頻繁創(chuàng)建局部引用的需要釋放
  2. 遍歷數(shù)組產(chǎn)生的局部引用,用完后要刪除。

全局引用

通過調(diào)用jobject NewGlobalRef(jobject obj)基于引用來創(chuàng)建,參數(shù)是jobject類型。它可以跨方法、跨線程使用。JVM不會自動釋放它,必須顯示調(diào)用DeleteGlobalRef手動釋放void DeleteGlobalRef(jobject globalRef)

如下使用示例:
在jni中調(diào)用java String類構(gòu)造返回String

// jni_ref.cpp
static jclass g_j_cls;  // 加static前綴 只對本源文件可見,對其它源文件隱藏
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jniGlobalRef(JNIEnv *env, jobject instance) {

    if (g_j_cls == nullptr) {
        jclass local_j_cls = env->FindClass("java/lang/String");
        // 將local_j_cls局部引用改為全局引用
        g_j_cls = (jclass) env->NewGlobalRef(local_j_cls);
    } else {
        LOG_D("g_j_cls else");
    }

    // 調(diào)用public String(String value); 構(gòu)造
    jmethodID j_mid = env->GetMethodID(g_j_cls, "<init>", "(Ljava/lang/String;)V");

    jstring str = env->NewStringUTF("GlobalRef");
    jstring j_str = (jstring) env->NewObject(g_j_cls, j_mid, str);
    return j_str;
}

extern "C" JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_delGlobalRef(JNIEnv *env, jobject instance) {
    if (g_j_cls != nullptr) {
        LOG_D("DeleteGlobalRef");
        // 釋放某個全局引用
        env->DeleteGlobalRef(g_j_cls);
    }
}

java調(diào)用

    public native String jniGlobalRef();
    public native void delGlobalRef();
    
    String ret1 = jniRef.jniGlobalRef();
    Log.e(TAG, "jniGlobalRef=" + ret1);
    String ret2 = jniRef.jniGlobalRef();
    Log.e(TAG, "jniGlobalRef=" + ret2);
    jniRef.delGlobalRef();

g_j_cls就是一個全局引用,然后我們多次調(diào)用下jniRef.jniGlobalRef方法打印如下:

2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp E/JNI: jniGlobalRef=GlobalRef
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp D/JNI: g_j_cls else
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp E/JNI: jniGlobalRef=GlobalRef
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp D/JNI: DeleteGlobalRef

說明全局引用可以起到緩存的效果,為什么要做這個測驗?zāi)兀?因為頻繁調(diào)用類似JNI接口FindClass查找java中Class引用時是比較耗性能的,特別是在有交互頻繁的JNI的app中。

弱全局引用

這個有點類似于java的軟引用SoftReference,jvm在內(nèi)存不足的時候會釋放它。通過調(diào)用jweak NewWeakGlobalRef(jobject obj)來創(chuàng)建一個弱全局引用,釋放調(diào)用void DeleteWeakGlobalRef(jweak obj),jweaktypedef _jobject* jweak;
_jobject指針的別名。

如下使用示例,和全局引用一樣把全局引用的方法改為弱全局引用的方法即可。

// jni_ref.cpp
static jclass g_w_j_cls;
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jniWeakGlobalRef(JNIEnv *env, jobject instance) {

    if (g_w_j_cls == nullptr) {
        jclass local_j_cls = env->FindClass("java/lang/String");
        // 將local_j_clss局部引用改為弱全局引用
        g_w_j_cls = (jclass) env->NewWeakGlobalRef(local_j_cls);
    } else {
        LOG_D("g_w_j_cls else");
    }

    jmethodID j_mid = env->GetMethodID(g_w_j_cls, "<init>", "(Ljava/lang/String;)V");

    // 使用弱引用時,必須先檢查緩存過的弱引用是指向活動的類對象,還是指向一個已經(jīng)被GC的類對象
    // 檢查弱引用是否活動,即引用的比較IsSameObject
    // 如果g_w_j_cls指向的引用已經(jīng)被回收,會返回JNI_TRUE
    // 如果仍然指向一個活動對象,會返回JNI_FALSE
    jboolean isGC = env->IsSameObject(g_w_j_cls, nullptr);
    if (isGC) {
        LOG_D("weak reference has been gc");
        return env->NewStringUTF("weak reference has been gc");
    } else {
        jstring str = env->NewStringUTF("WeakGlobalRef");
        jstring j_str = (jstring) env->NewObject(g_w_j_cls, j_mid, str);
        return j_str;
    }
}

extern "C" JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_delWeakGlobalRef(JNIEnv *env, jobject instance) {
    if (g_w_j_cls != nullptr) {
        // 調(diào)用DeleteWeakGlobalRef來釋放它,如果不手動調(diào)用這個函數(shù)來釋放所指向的對象,JVM仍會回收弱引用所指向的對象,但弱引用本身在引用表中所占的內(nèi)存永遠也不會被回收。
        LOG_D("DeleteWeakGlobalRef");
        env->DeleteWeakGlobalRef(g_w_j_cls);
    }
}

java調(diào)用

        String ret3 = jniRef.jniWeakGlobalRef();
        Log.e(TAG, "jniWeakGlobalRef=" + ret3);
        String ret4 = jniRef.jniWeakGlobalRef();
        Log.e(TAG, "jniWeakGlobalRef=" + ret4);
        jniRef.delWeakGlobalRef();

g_w_j_cls就是一個弱全局引用,然后我們多次調(diào)用下jniRef.jniWeakGlobalRef方法打印如下:

2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp E/JNI: jniWeakGlobalRef=WeakGlobalRef
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp D/JNI: g_w_j_cls else
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp E/JNI: jniWeakGlobalRef=WeakGlobalRef
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp D/JNI: DeleteWeakGlobalRef

和全局引用一樣可以起到緩存的效果。
剛才我們說了就是弱全局引用在內(nèi)存不足的時候會被jvm回收,怎么判斷它被回收了,判null ,沒錯!當(dāng)被回收了會為null。所以我們在使用弱全局引用的時候頻道弱全局引用是否還存在。怎么判斷呢?使用引用比較 。

引用比較

在jni中提供了 jboolean IsSameObject(jobject ref1, jobject ref2)方法。如果ref1和ref2指向同個對象則返回JNI_TRUE,否則返回JNI_FALSE。

    jclass local_j_cls_1 = env->FindClass("java/util/ArrayList");
    jclass local_j_cls_2 = env->FindClass("java/util/ArrayList");
    jboolean same1 = env->IsSameObject(local_j_cls_1, local_j_cls_2);
    LOG_D("%d",same1);
    jboolean same2= env->IsSameObject(local_j_cls_1, nullptr);
    LOG_D("%d",same2);

輸出 1和0

緩存策略

當(dāng)我們在本地代碼方法中通過FindClass查找Class、GetMethodID查找方法、GetFieldID獲取類的字段ID和GetFieldValue獲取字段的時候是需要jvm來做很多工作的,可能這個字段ID或者方法是在超類中繼承而來的,那jvm可能還需要層次遍歷。而這些負責(zé)和jni交互java中的類的全路徑,字段,方法一般是不會修改了,是固定的。這也是為什么我們在做android混淆打包的時候需要keep這些類,因為這些一般不會變,不能變,變了后jni中會找不到了具體的類,字段,方法了。既然打包后不會變我們是可以進行緩存策略來處理。

另外至于效率提高多少,沒有驗證,不過不重要,如果是頻繁這種查找一般會采用緩存,只查找一次或者在程序初始化的時候提前查找。

對于這類情況的緩存分為基本數(shù)據(jù)類型緩存和引用緩存。

基本數(shù)據(jù)類型緩存

基本數(shù)據(jù)類型的緩存在c,c++中可以借助關(guān)鍵字static處理。
學(xué)過c,c++的都知道

  1. static局部變量只初始化一次,下一次依據(jù)上一次結(jié)果值
  2. static全局變量只初使化一次,防止在其他文件中被引用
  3. 加static函數(shù)的函數(shù)為內(nèi)部函數(shù),只能在本源文件中使用, 和普通函數(shù)的作用域不同
static jclass g_j_cls_cache;
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_refCache(JNIEnv *env, jobject instance) {
    if (g_j_cls_cache == nullptr) {
        jclass local_j_cls = env->FindClass("java/lang/String");
        // 將local_j_cls局部引用改為全局引用
        g_j_cls_cache = (jclass) env->NewGlobalRef(local_j_cls);
    } else {
        LOG_D("g_j_cls_cache use cache");
    }

    // 調(diào)用public String(String value); 構(gòu)造
    static jmethodID j_mid;
    if (j_mid == nullptr) {
        j_mid = env->GetMethodID(g_j_cls_cache, "<init>", "(Ljava/lang/String;)V");
    } else {
        LOG_D("j_mid use cache");
    }

    jstring str = env->NewStringUTF("refCache");
    jstring j_str = (jstring) env->NewObject(g_j_cls_cache, j_mid, str);
    return j_str;
}

java調(diào)用

        String ret5 = jniRef.refCache();
        Log.e(TAG, "refCache=" + ret5);
        String ret6 = jniRef.refCache();
        Log.e(TAG, "refCache=" + ret6);
        jniRef.delRefCache();

local_j_cls局部引用變?yōu)槿忠茫琷_mid變量改為static
輸出:

10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp E/JNI: refCache=refCache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp D/JNI: g_j_cls_cache use cache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp D/JNI: j_mid use cache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp E/JNI: refCache=refCache

有人問local_j_cls局部引用可以加static嗎?不用全局引用/全局弱應(yīng)用? 可以加static,但是不能起到緩存的作用。因為上文說了局部引用在函數(shù)結(jié)束后會被jvm回收了,不然再次使用回到非法內(nèi)存訪問導(dǎo)致應(yīng)用crash,所以正確的做法如上用全局引用/全局弱應(yīng)用。

引用類型的緩存

可以借助上面的全局引用或者弱全局引用,弱全局引用記得在使用前判斷下是否被回收了IsSameObject,最后記得釋放 DeleteGlobalRef ,DeleteWeakGlobalRef。

最后源代碼:https://github.com/ta893115871/JNIAPP

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

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