人間觀察
好像什么都來得及,又好像什么都來不及,
本篇文章主要介紹在jni開發中常見的三種參考的使用方法和注意事項以及jni和java互動的快取策略,
我們知道Java是一門純面象物件的語言,除了基本資料型別外,其它任何型別所創建的物件的記憶體都存在堆空間中,記憶體由JVM 的GC(Garbage Collection)垃圾回收進行管理,
但是對于c,c++中以及用c/c++撰寫的jni來說同樣需要手動管理和處理記憶體,特別是參考型別的物件,malloc,realloc,free ,delete ,不像java有jvm對每個行程記憶體的限制,特別是Android移動端使用不當給你oom好不啦,而在c++/c只要你想要多大的來隨便搞(只要系統記憶體充足)但是需要釋放,各有各的優勢,
在jni中分為區域參考,全域參考,全域弱參考,個人認為有點類似于java中
區域參考,強參考,軟參考SoftReference,在使用介紹之前我們先看一下jni中的基本型別和參考型別有哪些以及對應關系,
jni資料型別
基本資料型別:
java與Native映射關系如下表所示:
| 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 |
參考資料型別
外面的為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的參考型別代表了繼承關系,jbooleanArray繼承jarray,jarray繼承jobject,最終都繼承jobject,
區域參考
通過呼叫jni的一些方法比如FindClass,NewCharArray,NewStringUTF等只要是回傳上面介紹的jni的參考型別都屬于區域參考,區域參考的生命周期只在方法中效,不能垮執行緒跨方法使用,函式退出后區域參考所參考的物件會被JVM自動釋放,或顯示呼叫DeleteLocalRef釋放,區域參考的也可以通過(*env)->NewLocalRef(env,local_ref)方法創建,一般不常用,
如下示例:
// jni_ref.cpp
// 在jni中呼叫java String類構造回傳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");
// 呼叫public String(char[] value); 構造方法, 為了演示更多的區域參考
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);
// 釋放區域參考,也可以不用呼叫在方法結束后jvm會自動回收,最好有良好的編碼習慣
env->DeleteLocalRef(local_j_cls);
env->DeleteLocalRef(local_str);
env->DeleteLocalRef(local_j_charArr);
// 也可以通過NewLocalRef函式創建 (*env)->NewLocalRef(env,local_ref);這個方法一般很少用,
// 函式回傳后區域參考所參考的物件會被JVM自動釋放,或呼叫DeleteLocalRef釋放,(*env)->DeleteLocalRef(env,local_ref)
// ReleaseStringChars和GetStringChars對應
env->ReleaseStringChars(j_str, j_char);
return j_str;
}
例子中的local_j_cls,local_j_charArr,local_j_charArr,j_str 都是區域參考型別,最后呼叫了DeleteLocalRef來釋放,
有同學問了,既然區域參考不用手動釋放,可不可以不用呼叫DeleteLocalRef方法,
咦,你這個小可愛,好問題哦!

我網上搜索了下,大部分的文章說了下會有限制,超過512個區域參考(為什么是這個數字,一看就是一個有情懷的程式員)會造成區域參考表溢位,我還是想測驗一下如下
// 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回圈900w次,例外出現了,
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 版本導致,Android經常這樣不同的API或者功能在不同的版本上表現不一樣, 而我我用的Android 8.1的系統,為什么512沒有報錯了,
Android 8.0 之前區域參考表的上限是512個參考,Android 8.0后區域參考表上限提升到了8388608個參考,大家想一探究竟的話可以在如下Android 底層代碼中看一下,
需要翻墻
有關底層原始碼
看原始碼的同時我們也看到了比如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);
}
綜上可以看出并不是區域參考不用呼叫DeleteLocalRef來釋放,而是建議呼叫一下,如果你的jni方法很簡單&與java互動很少也可以不呼叫,但是如下的一些情況需要手動顯示的呼叫,為了防止記憶體溢位和區域參考表溢位,
- 如上我們模擬的情況,在for回圈里或者其它操作類似頻繁創建區域參考的需要釋放
- 遍歷陣列產生的區域參考,用完后要洗掉,
全域參考
通過呼叫jobject NewGlobalRef(jobject obj)基于參考來創建,引數是jobject型別,它可以跨方法、跨執行緒使用,JVM不會自動釋放它,必須顯示呼叫DeleteGlobalRef手動釋放void DeleteGlobalRef(jobject globalRef)
如下使用示例:
在jni中呼叫java String類構造回傳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");
}
// 呼叫public String(String value); 構造
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呼叫
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就是一個全域參考,然后我們多次呼叫下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
說明全域參考可以起到快取的效果,為什么要做這個測驗呢? 因為頻繁呼叫類似JNI介面FindClass查找java中Class參考時是比較耗性能的,特別是在有互動頻繁的JNI的app中,
弱全域參考
這個有點類似于java的軟參考SoftReference,jvm在記憶體不足的時候會釋放它,通過呼叫jweak NewWeakGlobalRef(jobject obj)來創建一個弱全域參考,釋放呼叫void DeleteWeakGlobalRef(jweak obj),jweak為typedef _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");
// 使用弱參考時,必須先檢查快取過的弱參考是指向活動的類物件,還是指向一個已經被GC的類物件
// 檢查弱參考是否活動,即參考的比較IsSameObject
// 如果g_w_j_cls指向的參考已經被回收,會回傳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) {
// 呼叫DeleteWeakGlobalRef來釋放它,如果不手動呼叫這個函式來釋放所指向的物件,JVM仍會回收弱參考所指向的物件,但弱參考本身在參考表中所占的記憶體永遠也不會被回收,
LOG_D("DeleteWeakGlobalRef");
env->DeleteWeakGlobalRef(g_w_j_cls);
}
}
java呼叫
String ret3 = jniRef.jniWeakGlobalRef();
Log.e(TAG, "jniWeakGlobalRef=" + ret3);
String ret4 = jniRef.jniWeakGlobalRef();
Log.e(TAG, "jniWeakGlobalRef=" + ret4);
jniRef.delWeakGlobalRef();
g_w_j_cls就是一個弱全域參考,然后我們多次呼叫下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
和全域參考一樣可以起到快取的效果,
剛才我們說了就是弱全域參考在記憶體不足的時候會被jvm回收,怎么判斷它被回收了,判null ,沒錯!當被回收了會為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
快取策略
當我們在本地代碼方法中通過FindClass查找Class、GetMethodID查找方法、GetFieldID獲取類的欄位ID和GetFieldValue獲取欄位的時候是需要jvm來做很多作業的,可能這個欄位ID或者方法是在超類中繼承而來的,那jvm可能還需要層次遍歷,而這些負責和jni互動java中的類的全路徑,欄位,方法一般是不會修改了,是固定的,這也是為什么我們在做android混淆打包的時候需要keep這些類,因為這些一般不會變,不能變,變了后jni中會找不到了具體的類,欄位,方法了,既然打包后不會變我們是可以進行快取策略來處理,
另外至于效率提高多少,沒有驗證,不過不重要,如果是頻繁這種查找一般會采用快取,只查找一次或者在程式初始化的時候提前查找,
對于這類情況的快取分為基本資料型別快取和參考快取,
基本資料型別快取
基本資料型別的快取在c,c++中可以借助關鍵字static處理,
學過c,c++的都知道
- static區域變數只初始化一次,下一次依據上一次結果值
- static全域變數只初使化一次,防止在其他檔案中被參考
- 加static函式的函式為內部函式,只能在本源檔案中使用, 和普通函式的作用域不同
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");
}
// 呼叫public String(String value); 構造
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呼叫
String ret5 = jniRef.refCache();
Log.e(TAG, "refCache=" + ret5);
String ret6 = jniRef.refCache();
Log.e(TAG, "refCache=" + ret6);
jniRef.delRefCache();
local_j_cls區域參考變為全域參考,j_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嗎?不用全域參考/全域弱應用? 可以加static,但是不能起到快取的作用,因為上文說了區域參考在函式結束后會被jvm回收了,不然再次使用回到非法記憶體訪問導致應用crash,所以正確的做法如上用全域參考/全域弱應用,
參考型別的快取
可以借助上面的全域參考或者弱全域參考,弱全域參考記得在使用前判斷下是否被回收了IsSameObject,最后記得釋放 DeleteGlobalRef ,DeleteWeakGlobalRef,
最后源代碼:https://github.com/ta893115871/JNIAPP
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/179221.html
標籤:其他
