Android NDK开发篇:如何使用JNI中的global reference和local reference
JNI提供了一些实例和数组类型(jobject、jclass、jstring、jarray等)作为不透明的引用供本地代码使用。本地代码永远不会直接操作引用指向的VM内部的数据内容。要进行这些操作,必须通过使用JNI操作一个不引用来间接操作数据内容。因为只操作引用,你不必担心特定JVM中对象的存储方式等信息。这样的话,你有必要了解一下JNI中的几种不同的引用:
1、 JNI支持三种引用:局部引用、全局引用、弱全局引用(下文简称“弱引用”)。
2、 局部引用和全局引用有不同的生命周期。当本地方法返回时,局部引用会被自动释放。而全局引用和弱引用必须手动释放。
3、 局部引用或者全局引用会阻止GC回收它们所引用的对象,而弱引用则不会。
4、 不是所有的引用可以被用在所有的场合。例如,一个本地方法创建一个局部引用并返回后,再对这个局部引用进行访问是非法的。
本章中,我们会详细地讨论这些问题。合理地管理JNI引用是写出高质量的代码的基础。
5.1 局部引用和全局引用
什么是全局引用和局部引用?它们有什么不同?我们下面使用一些例子来说明。
1 /* 局部引用 */ 2 jclass classLocal = env->FindClass("java/lang/String"); 3 env->DeleteLocalRef(classLocal); 4 /* 全局引用 */ 5 jclass classLocal = env->FindClass("com/kvdb/NativeKVDB"); 6 jclass classGlobal = (jclass) env->NewGlobalRef(classLocal); 7 env->DeleteGlobalRef(classGlobal); 8 /* 弱引用 */ 9 jclass classLocal =env->FindClass("mypkg/MyCls2"); 10 jclass classGlobal = NewWeakGlobalRef(env, classLocal); 11 env->DeleteGlobalRef(classGlobal);
5.1.1 局部引用
大多数JNI函数会创建局部引用。例如,NewObject创建一个新的对象实例并返回一个对这个对象的局部引用。
局部引用只有在创建它的本地方法返回前有效。本地方法返回后,局部引用会被自动释放。
你不能在本地方法中把局部引用存储在静态变量中缓存起来供下一次调用时使用。
1 /* This code is illegal */ 2 jstring MyNewString(JNIEnv *env, jchar *chars, jint len) { 3 static jclass stringClass = NULL; 4 jmethodID cid; 5 jcharArray elemArr; 6 jstring result; 7 stringClass = (*env)->FindClass(env, "java/lang/String"); 8 if (stringClass == NULL) { 9 return NULL; /* exception thrown */ 10 } 11 cid = (*env)->GetMethodID(env, stringClass, 12 13 "<init>", "([C)V"); 14 15 ... 16 elemArr = (*env)->NewCharArray(env, len); 17 ... 18 result = (*env)->NewObject(env, stringClass, cid, elemArr); 19 (*env)->DeleteLocalRef(env, elemArr); 20 return result; 21 }
上面代码中,我们省略了和我们的讨论无关的代码。假设一个本地方法C.f调用了MyNewString:
JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) {
char *c_str = ...;
...
return MyNewString(c_str);
}
C.f方法返回后,VM释放了在这个方法执行期间创建的所有局部引用,也包含对String类的引用stringClass。
释放一个局部引用有两种方式,一个是本地方法执行完毕后VM自动释放,另外一个是程序员通过DeleteLocalRef手动释放。
既然VM会自动释放局部引用,为什么还需要手动释放呢?因为局部引用会阻止它所引用的对象被GC回收。
局部引用只在创建它们的线程中有效,跨线程使用是被禁止的。不要在一个线程中创建局部引用并存储到全局引用中,然后到另外一个线程去使用。
5.1.2全局引用
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,全局引用也会阻止它所引用的对象被GC回收。
与局部引用可以被大多数JNI函数创建不同,全局引用只能使用一个JNI函数创建:NewGlobalRef。下面这个版本的MyNewString演示了怎么样使用一个全局引用:
1 /* This code is OK */ 2 jstring MyNewString(JNIEnv *env, jchar *chars, jint len) { 3 static jclass stringClass = NULL; 4 ... 5 if (stringClass == NULL) { 6 jclass localRefCls = 7 (*env)->FindClass(env, "java/lang/String"); 8 if (localRefCls == NULL) { 9 return NULL; /* exception thrown */ 10 } 11 /* Create a global reference */ 12 stringClass = (*env)->NewGlobalRef(env, localRefCls); 13 /* The local reference is no longer useful */ 14 (*env)->DeleteLocalRef(env, localRefCls); 15 /* Is the global reference created successfully? */ 16 if (stringClass == NULL) { 17 return NULL; /* out of memory exception thrown */ 18 } 19 } 20 ... 21 }
上面这段代码中,一个由FindClass返回的局部引用被传入NewGlobalRef,用来创建一个对String类的全局引用。删除localRefCls后,我们检查NewGlobalRef是否成功创建stringClass。
5.1.3弱引用
弱引用使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象。
在MyNewString中,我们也可以使用弱引用来存储stringClass这个类引用,因为java.lang.String这个类是系统类,永远不会被GC回收。
当本地代码中缓存的引用不一定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法mypkg.MyCls.f需要缓存一个指向类mypkg.MyCls2的引用,如果在弱引用中缓存的话,仍然允许mypkg.MyCls2这个类被unload:
1 JNIEXPORT void JNICALL Java_mypkg_MyCls_f(JNIEnv *env, jobject self) { 2 static jclass myCls2 = NULL; 3 if (myCls2 == NULL) { 4 jclass myCls2Local = 5 (*env)->FindClass(env, "mypkg/MyCls2"); 6 if (myCls2Local == NULL) { 7 return; /* can't find class */ 8 } 9 myCls2 = NewWeakGlobalRef(env, myCls2Local); 10 if (myCls2 == NULL) { 11 return; /* out of memory */ 12 } 13 } 14 ... /* use myCls2 */ 15 }
我们假设MyCls和MyCls2有相同的生命周期(例如,他们可能被相同的类加载器加载),因为弱引用的存在,我们不必担心MyCls和它所在的本地代码在被使用时,MyCls2这个类出现先被unload,后来又会preload的情况。
当然,真的发生这种情况时(MyCls和MyCls2的生命周期不同),我们必须检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC给unload的类对象。下一节将告诉你怎么样检查弱引用是否活动。