在 Native 代码中有时候会接收 Java 传入的引用类型参数,有时候也会经过 NewObject 方法来建立一个 Java 的引用类型变量。java
在编写 Native 代码时,要注意这个表明 Java 数据结构类型的引用在使用时会被 GC 回收的可能性。android
Native 代码并不能直接经过引用来访问其内部的数据接口,必需要经过调用 JNI 接口来间接操做这些引用对象,就如在以前的系列文章中写的那样。而且 JNI 还提供了和 Java 相对应的引用类型,所以,咱们就须要经过管理好这些引用来管理 Java 对象,避免在使用时被 GC 回收了。git
JNI 提供了三种引用类型:github
局部引用是最多见的一种引用。绝大多数 JNI 函数建立的都是局部引用,好比:NewObject、FindClass、NewObjectArray 函数等等。缓存
局部引用会阻止 GC 回收所引用的对象,同时,它不能在本地函数中跨函数传递,不能跨线程使用。
局部引用在 Native 函数返回后,所引用的对象会被 GC 自动回收,也能够经过 DeleteLocalRef 函数来手动回收。微信
在以前文章 JNI 调用时缓存字段和方法 ID,第一种方法采用的是使用时缓存,把字段 ID 经过 static 变量缓存起来。数据结构
若是把 FindClass 函数建立的局部引用也经过 static 变量缓存起来,那么在函数退出后,局部引用被自动释放了,static 静态变量中存储的就是一个被释放后的内存地址,成为了一个野指针,再次调用时就会引发程序崩溃了。函数
extern "C" JNIEXPORT jstring JNICALL Java_com_glumes_cppso_jnioperations_LocalAndGlobalReferenceOps_errorCacheUseLocalReference( JNIEnv *env, jobject instance) { static jmethodID mid = NULL; static jclass cls; // 局部引用不能使用 static 来缓存,不然函数退出后,自动释放,成为野指针,程序 Crash if (cls == NULL) { cls = env->FindClass("java/lang/String"); if (cls == NULL) { return NULL; } } else { LOGD("cls is not null but program will crash"); } if (mid == NULL) { mid = env->GetMethodID(cls, "<init>", "([C)V"); if (mid == NULL) { return NULL; } } jcharArray charEleArr = env->NewCharArray(10); const jchar *j_char = env->GetStringChars(env->NewStringUTF("LocalReference"), NULL); env->SetCharArrayRegion(charEleArr, 0, 10, j_char); jstring result = (jstring) env->NewObject(cls, mid, charEleArr); env->DeleteLocalRef(charEleArr); return result; }
局部引用除了自动释放外,还能够经过 DeleteLocalRef 函数手动释放,它通常存在于如下场景中:工具
假若有如下代码,则要特别注意,及时释放局部引用,防止溢出。post
for (int i = 0; i < len; ++i) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* process jstr */ (*env)->DeleteLocalRef(env, jstr); }
在编写工具类时,很难知道被调用者具体会是谁,考虑到通用性,完成工具类的任务以后,就要及时释放相应的局部引用,防止被占着内存空间。
若是 Native 方法不会返回,那么自动释放局部引用就失效了,这时候就必需要手动释放。好比,在某个一直等待的循环中,若是不及时释放局部引用,很快就会溢出了。
好比,经过局部引用建立了一个大对象,而后这个对象在函数中间就完成了任务,那么就能够早早地经过手动释放了,而不是等到函数的结尾才释放。
Java 还提供了一些函数来管理局部引用的生命周期:
JNI 的规范指出,JVM 要确保每一个 Native 方法至少能够建立 16 个局部引用,经验代表,16 个局部引用已经足够日常的使用了。
可是,若是要与 JVM 的中对象进行复杂的交互计算,就须要建立更多的局部引用了,这时就须要使用 EnsureLocalCapacity
来确保能够建立指定数量的局部引用,若是建立成功返回 0 ,返回返回小于 0 ,以下代码示例:
// Use EnsureLocalCapacity int len = 20; if (env->EnsureLocalCapacity(len) < 0) { // 建立失败,out of memory } for (int i = 0; i < len; ++i) { jstring jstr = env->GetObjectArrayElement(arr,i); // 处理 字符串 // 建立了足够多的局部引用,这里就不用删除了,显然占用更多的内存 }
引用确保能够建立了足够的局部引用数量,因此在循环处理局部引用时能够不进行删除了,可是显然会消耗更多的内存空间了。
PushLocalFrame 与 PopLocalFrame 是两个配套使用的函数对。
它们能够为局部引用建立一个指定数量内嵌的空间,在这个函数对之间的局部引用都会在这个空间内,直到释放后,全部的局部引用都会被释放掉,不用再担忧每个局部引用的释放问题了。
常见的使用场景就是在循环中:
// Use PushLocalFrame & PopLocalFrame for (int i = 0; i < len; ++i) { if (env->PushLocalFrame(len)) { // 建立指定数据的局部引用空间 //out ot memory } jstring jstr = env->GetObjectArrayElement(arr, i); // 处理字符串 // 期间建立的局部引用,都会在 PushLocalFrame 建立的局部引用空间中 // 调用 PopLocalFrame 直接释放这个空间内的全部局部引用 env->PopLocalFrame(NULL); }
使用 PushLocalFrame & PopLocalFrame 函数对,就能够在期间放心地处理局部引用,最后统一释放掉。
全局引用和局部引用同样,也会阻止它所引用的对象被回收。可是它不会在方法返回时被自动释放,必需要经过手动释放才行,并且,全局引用能够跨方法、跨线程使用。
全局引用只能经过 NewGlobalRef
函数来建立,而后经过 DeleteGlobalRef
函数来手动释放。
仍是上面提到的缓存字段的例子,如今就可使用全局引用来缓存了。
extern "C" JNIEXPORT jstring JNICALL Java_com_glumes_cppso_jnioperations_LocalAndGlobalReferenceOps_cacheWithGlobalReference(JNIEnv *env, jobject instance) { static jclass stringClass = NULL; if (stringClass == NULL) { jclass localRefs = env->FindClass("java/lang/String"); if (localRefs == NULL) { return NULL; } stringClass = (jclass) env->NewGlobalRef(localRefs); env->DeleteLocalRef(localRefs); if (stringClass == NULL) { return NULL; } } else { LOGD("use stringClass cached"); } static jmethodID stringMid = NULL; if (stringMid == NULL) { stringMid = env->GetMethodID(stringClass, "<init>", "(Ljava/lang/String;)V"); if (stringMid == NULL) { return NULL; } } else { LOGD("use method cached"); } jstring str = env->NewStringUTF("string"); return (jstring) env->NewObject(stringClass, stringMid, str); }
弱全局引用有点相似于 Java 中的弱引用,它所引用的对象能够被 GC 回收,而且它也能够跨方法、跨线程使用。
弱引用使用 NewWeakGlobalRef
方法建立,使用 DeleteWeakGlobalRef
方法释放。
extern "C" JNIEXPORT void JNICALL Java_com_glumes_cppso_jnioperations_LocalAndGlobalReferenceOps_useWeakGlobalReference(JNIEnv *env, jobject instance) { static jclass stringClass = NULL; if (stringClass == NULL) { jclass localRefs = env->FindClass("java/lang/String"); if (localRefs == NULL) { return; } stringClass = (jclass) env->NewWeakGlobalRef(localRefs); if (stringClass == NULL) { return; } } static jmethodID stringMid = NULL; if (stringMid == NULL) { stringMid = env->GetMethodID(stringClass, "<init>", "(Ljava/lang/String;)V"); if (stringMid == NULL) { return; } } jboolean isGC = env->IsSameObject(stringClass, NULL); if (isGC) { LOGD("weak reference has been gc"); } else { jstring str = (jstring) env->NewObject(stringClass, stringMid, env->NewStringUTF("jstring")); LOGD("str is %s", env->GetStringUTFChars(str, NULL)); } }
在使用弱引用时,要先检查弱引用所指的类对象是否被 GC 给回收了。经过 isSameObject
方法进行检查。
isSameObject
方法能够用来比较两个引用类型是否相同,也能够用来比较引用是否为 NULL。同时,还能够用 isSameObject
来比较弱全局引用所引用的对象是否被 GC 了,返回 JNI_TRUE 则表示回收了,JNI_FALSE 则表示未被回收。
env->IsSameObject(obj1, obj2) // 比较局部引用 和 全局引用是否相同 env->IsSameObject(obj, NULL) // 比较局部引用或者全局引用是否为 NULL env->IsSameObject(wobj, NULL) // 比较弱全局引用所引用对象是否被 GC 回收
总结一些关于引用管理方面的知识点,能够减小内存的使用和避免由于对象被引用不能释放而形成的内存浪费。
一般来讲,Native 代码大致有两种状况:
对于直接实现 Java 层声明的 Native 函数,不要形成全局引用和弱全局引用的累加,由于它们在函数返回后并不会自动释放。
对于工具类的 Native 函数,因为它的调用场合和次数是不肯定的,因此要格外当心各类引用类型,避免形成累积而致使内存溢出,好比以下规则:
同时,对于工具类的 Native 函数,使用缓存技术来保存一些全局引用也是可以提升效率的,正如 Android JNI 调用时缓存字段和方法 ID 文章中写到的同样。同时,在工具类中,若是返回的是引用类型,最好说明返回的引用是哪种类型,以下代码所示:
while (JNI_TRUE) { jstring infoString = GetInfoString(info); ... /* process infoString */ ??? /* we need to call DeleteLocalRef, DeleteGlobalRef, or DeleteWeakGlobalRef depending on the type of reference returned by GetInfoString. */ }
由于不清楚返回的引用是什么类型,致使不知道调用哪一种方式来删除这个引用类型。
JNI 中提供了 NewLocalRef
函数来保证工具类函数返回的老是一个局部引用类型,以下代码:
static jstring cachedString = NULL; if (cachedString == NULL) { /* create cachedString for the first time */ jstring cachedStringLocal = ... ; /* cache the result in a global reference */ cachedString =(*env)->NewGlobalRef(env, cachedStringLocal); } return (*env)->NewLocalRef(env, cachedString);
正如以前提到的,static 缓存是不能缓存一个局部引用的,而 cachedString 正是一个缓存的全局引用,可是经过 NewLocalRef 函数确保将返回的是一个局部引用,这样再使用后局部引用就会被自动释放掉了。
对于引用的管理,最好的方式仍是使用 PushLocalFrame
与 PopLocalFrame
函数对,在这个函数对之间的局部引用就能够自动被 PushLocalFrame 和 PopLocalFrame 接管了。
具体示例代码可参考个人 Github 项目,欢迎 Star。
https://github.com/glumes/AndroidDevWithCpp
欢迎关注微信公众号:【纸上浅谈】,得到最新文章推送~~