面試的時候遇到一些候選人的簡歷上寫著熟悉jni,但是問的時候才發(fā)現(xiàn)對jni的了解僅僅是停留在java和c的方法是如何相互調(diào)用上躺孝。其實這遠遠稱不上熟悉,這篇博客就來講講jni面試中經(jīng)常還會問到的內(nèi)存管理問題。
首先我們知道java和c的對象是不能直接共用的,例如字符串我們不能直接返回char*,而需要創(chuàng)建一個jstring對象:
std::string hello = "hello world";
jstring jstr = env->NewStringUTF(hello.c_str());
那問題就來了,這個jstr是我們用env去new出來的。那我們需要手動去delete嗎,不delete會不會造成內(nèi)存泄露?
如果需要的話,當我們需要將這個jstr返回給java層使用的時候又要怎么辦呢?不delete就內(nèi)存泄露,delete就野指針:
extern "C" JNIEXPORT jstring JNICALL
Java_me_linjw_ndkdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject thiz/* this */) {
std::string hello = "hello world";
jstring jstr = env->NewStringUTF(hello.c_str());
return jstr;
}
其實jni為了解決這個問題,設計了三種引用類型:
- 局部引用
- 全局引用
- 弱全局引用
局部引用
我們先從局部引用講起,其實我們這里通過NewStringUTF創(chuàng)建的jstring就是局部引用,那它有什么特點呢?
我們在c層大多數(shù)調(diào)用jni方法創(chuàng)建的引用都是局部引用,它會別存放在一張局部引用表里她倘。它的內(nèi)存有四種釋放方式:
- 程序員可以手動調(diào)用DeleteLocalRef去釋放
- c層方法執(zhí)行完成返回java層的時候,jvm會遍歷局部引用表去釋放
- 使用PushLocalFrame/PopLocalFrame創(chuàng)建/銷毀局部引用棧幀的時候,在PopLocalFrame里會釋放幀內(nèi)創(chuàng)建的引用
- 如果使用AttachCurrentThread附加原生線程,在調(diào)用DetachCurrentThread的時候會釋放該線程創(chuàng)建的局部引用
所以上面的問題我們就能回答了, jstr可以不用手動delete,可以等方法結(jié)束的時候jvm自己去釋放(當然如果返回之后在java層將這個引用保存了起來,那也是不會立馬釋放內(nèi)存的)
但是這樣是否就意味著我們可以任性的去new對象,不用考慮任何東西呢?其實也不是,局部引用表是有大小限制的,如果new的內(nèi)存太多的話可能造成局部引用表的內(nèi)存溢出,例如我們在for循環(huán)里面不斷創(chuàng)建對象:
std::string hello = "hello world";
for(int i = 0 ; i < 9999999 ; i ++) {
env->NewStringUTF(hello.c_str());
}
這就會引起local reference table overflow:
所以在使用完之后一定記得調(diào)用DeleteLocalRef去釋放它瘦癌。
有些同學可能會說,怎么可能會有人真的直接就在循環(huán)里不斷創(chuàng)建對象呢看锉。其實這種溢出大多數(shù)情況發(fā)生在被循環(huán)調(diào)用的方法里面:
void func(JNIEnv *env) {
std::string hello = "hello world";
env->NewStringUTF(hello.c_str());
}
...
for(int i = 0 ; i < 9999999 ; i ++) {
func(env);
}
作為一個安全的程序員,在對象不再使用的時候,立馬使用DeleteLocalRef去將其釋放是一個很好的習慣。
局部引用棧幀
如上面所說我們可能在某個函數(shù)中創(chuàng)建了局部引用,然后這個函數(shù)在循環(huán)中被調(diào)用,就容易出現(xiàn)溢出搪缨。
但是如果方法里面創(chuàng)建了多個局部引用,在return之前一個個去釋放會顯得十分繁瑣:
void func(JNIEnv *env) {
...
jstring jstr1 = env->NewStringUTF(str1.c_str());
jstring jstr2 = env->NewStringUTF(str2.c_str());
jstring jstr3 = env->NewStringUTF(str3.c_str());
jstring jstr4 = env->NewStringUTF(str4.c_str());
...
env->DeleteLocalRef(jstr1);
env->DeleteLocalRef(jstr2);
env->DeleteLocalRef(jstr3);
env->DeleteLocalRef(jstr4);
}
這個時候可以考慮使用局部引用棧幀:
void func(JNIEnv *env) {
env->PushLocalFrame(4);
...
jstring jstr1 = env->NewStringUTF(str1.c_str());
jstring jstr2 = env->NewStringUTF(str2.c_str());
jstring jstr3 = env->NewStringUTF(str3.c_str());
jstring jstr4 = env->NewStringUTF(str4.c_str());
...
env->PopLocalFrame(NULL);
}
我們在方法開頭PushLocalFrame,結(jié)尾PopLocalFrame,這樣整個方法就在一個局部引用幀里面,而在PopLocalFrame就會將該幀里面創(chuàng)建的局部引用全部釋放。
有的同學可能會想到一種場景,如果需要將某個局部引用當初返回值返回怎么辦?用局部引用幀會不會造成野指針?
其實jni也考慮到了這中情況,所以PopLocalFrame有一個參數(shù):
jobject PopLocalFrame(jobject result)
這個result參數(shù)可以傳入你的返回值引用,這樣的話這個局部引用就會在去到父幀里面,這樣就能直接返回了:
jstring func(JNIEnv *env) {
env->PushLocalFrame(4);
...
jstring jstr1 = env->NewStringUTF(str1.c_str());
jstring jstr2 = env->NewStringUTF(str2.c_str());
jstring jstr3 = env->NewStringUTF(str3.c_str());
jstring jstr4 = env->NewStringUTF(str4.c_str());
...
return (jstring)env->PopLocalFrame(jstr4);
}
PS: 就算使用了result參數(shù),局部引用幀里面的引用也是會失效的,所以不能直接將它返回,而是需要用PopLocalFrame為它創(chuàng)建的新引用,這個引用才在父幀里面鸵熟。
多線程下的局部引用
前面三種情況我們好理解,但是第四種情況又是什么意思呢?
3.如果使用AttachCurrentThread附加原生線程,在調(diào)用DetachCurrentThread的時候會釋放該線程創(chuàng)建的局部引用
我們使用JNIEnv這個數(shù)據(jù)結(jié)構(gòu)去調(diào)用jni的方法創(chuàng)建局部引用,但是JNIEnv將用于線程本地存儲,所以我們不能在線程之間共享它副编。
如果是java層創(chuàng)建的線程,那調(diào)到c層會自然傳入一個JNIEnv指針,但是如果是我們在c層自己新建的線程,我們要怎么拿的這個線程的JNIEnv呢?
在講解之前還有一個知識點要先交代,除了JNIEnv其實jni還有個很重要的數(shù)據(jù)結(jié)構(gòu)JavaVM,理論上每個進程可以有多個JavaVM,但Android只允許有一個,所以JavaVM是可以在多線程間共享的。
我們在java層使用System.loadLibrary方法加載so的時候,c層的JNI_OnLoad方法會被調(diào)用,我們可以在拿到JavaVM指針并將它保存起來:
JavaVM* g_Vm;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_Vm = vm;
return JNI_VERSION_1_4;
}
之后可以在線程中使用它的AttachCurrentThread方法附加原生線程,然后在線程結(jié)束的時候使用DetachCurrentThread去解除附加:
pthread_t g_pthread;
JavaVM* g_vm;
void* ThreadRun(void *data) {
JNIEnv* env;
g_vm->AttachCurrentThread(&env, nullptr);
...
g_vm->DetachCurrentThread();
}
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
return JNI_VERSION_1_4;
}
...
pthread_create(&g_pthread, NULL, ThreadRun, (void *) 1);
所以在AttachCurrentThread和DetachCurrentThread之間JNIEnv都是有效的,我們可以使用它去創(chuàng)建局部引用,而在DetachCurrentThread之后JNIEnv就失效了,同時我們用它創(chuàng)建的局部引用也會被回收流强。
全局引用
假設我們需要使用監(jiān)聽者模式在c層保存java對象的引用,并啟動線程執(zhí)行操作,在適當?shù)臅r候通知java層痹届。我們需要怎么做,一種<font color='red'>錯誤</font>的做法是直接將傳入的jobject保存到全局變量:
jobject g_listener;
extern "C" JNIEXPORT void JNICALL
Java_me_linjw_ndkdemo_MainActivity_registerListener(
JNIEnv *env,
jobject thiz,
jobject listener) {
g_listener = listener; // 錯誤的做法!!!
}
原因是這里傳進來的jobject其實也是局部引用,而局部引用是不能跨線程使用的。我們應該將它轉(zhuǎn)換成全局引用去保存:
jobject g_listener;
extern "C" JNIEXPORT void JNICALL
Java_me_linjw_ndkdemo_MainActivity_registerListener(
JNIEnv *env,
jobject thiz,
jobject listener) {
g_listener = env->NewGlobalRef(listener);
}
顧名思義,全局引用就是全局存在的引用,只有在我們調(diào)用DeleteGlobalRef之后它才會失效打月。
然后這樣又出現(xiàn)了個問題,按道理這個g_listener和listener應該指向的是同一個java對象,但是如果我們這樣去判斷的話是錯誤的:
if(g_listener == listener) {
...
}
它們的值是不會相等的,如果要判斷兩個jobject是否指向同一個java對象要需要用IsSameObject去判斷:
if(env->IsSameObject(g_listener, listener)) {
...
}
弱全局引用
弱全局引用和全局引用類似,可以在跨線程使用,它使用NewGlobalWeakRef創(chuàng)建,使用DeleteGlobalWeakRef釋放队腐。
但是弱全局引用是會被gc回收的,所以在使用的時候我們需要先判斷它是否已經(jīng)被回收:
if(!env->IsSameObject(g_listener, NULL)) {
...
}
JNI中的NULL引用指向JVM中的null對象。