望远山,知近路,而后自得其乐!

Android JNI开发详解(5)-引用篇

在JNI规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。

1. 局部引用

1.1 局部引用

通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线程使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef主动释放 。

jclass jString = env->FindClass("java/lang/String");
jcharArray charArray = env->NewCharArray(len);
jstring jstr = env->NewStringUTF("hello world");
jstring strlocalRef = env->NewLocalRef(jString);   // 通过NewLocalRef创建本地引用
...
DeleteLocalRef(env,strlocalRef);

局部引用也称本地引用,通常是在函数中创建并使用。会阻止GC回收所引用的对象。比如,调用NewObject接口创建一个新的对象实例并返回一个对这个对象的局部引用。局部引用只有在创建它的本地方法返回前有效,*本地方法返回到Java层之后,如果Java层没有对返回的局部引用使用的话*,局部引用就会被JVM自动释放。你可能会为了提高程序的性能,在函数中将局部引用存储在静态变量中缓存起来,供下次调用时使用。这种方式是错误的,因为函数返回后局部引很可能马上就会被释放掉,静态变量中存储的就是一个被释放后的内存地址,成了一个野针对,下次再使用的时候就会造成非法地址的访问,使程序崩溃。

1.2 释放局部引用

释放一个局部引用有两种方式,一个是本地方法执行完毕后JVM自动释放,另外一个是自己调用DeleteLocalRef手动释放。既然JVM会在函数返回后会自动释放所有局部引用,为什么还需要手动释放呢?大部分情况下,我们在实现一个本地方法时不必担心局部引用的释放问题,函数被调用完成后,JVM 会自动释放函数中创建的所有局部引用。但在某些情况下,我们必须要手动调用DeleteLocalRef释放,否则会引起运行奔溃。JNI会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。Android上的JNI局部引用表最大数量是512个。当我们在实现一个本地方法时,可能需要创建大量的局部引用,如果没有及时释放,就有可能导致JNI局部引用表的溢出,所以,在不需要局部引用时就立即调用DeleteLocalRef手动删除。比如,在下面的代码中,本地代码遍历一个特别大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对使用完这个元素的局部引用时,就应该马上手动释放它。

for (i = 0; i < len; i++) {
     jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
     ...
     (*env)->DeleteLocalRef(env, jstr); // 使用完成之后马上释放
}

另外,如果使用 AttachCurrentThread 附加原生线程,那么在线程分离之前,您运行的代码将绝不会自动释放局部引用。您创建的任何局部引用都必须手动删除。

1.3 局部有效性

传递给原生方法的每个参数,以及 JNI 函数返回的几乎每个对象都属于“局部引用”。这意味着,局部引用在当前线程中的当前原生方法运行期间有效。 在原生方法返回后,即使对象本身继续存在,该引用也无效。 局部引用不能跨线程使用,只在创建它的线程有效。不要试图在一个线程中创建局部引用并存储到全局引用中,然后在另外一个线程中使用。

1.4 管理局部引用

JNI提供了一系列函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI规范指出,任何实现JNI规范的JVM,必须确保每个本地函数至少可以创建16个局部引用(可以理解为虚拟机默认支持创建16个局部引用)。实际经验表明,这个数量已经满足大多数不需要和JVM中内部对象有太多交互的本地方函数。如果需要创建更多的引用,可以通过调用EnsureLocalCapacity函数,确保在当前线程中创建指定数量的局部引用,如果创建成功则返回0,否则创建失败,并抛出OutOfMemoryError异常。 在下面的代码中,遍历数组时会获取每个元素的引用,使用完了之后不手动删除,不考虑内存因素的情况下,它可以为这种创建大量的局部引用提供足够的空间。由于没有及时删除局部引用,因此在函数执行期间,会消耗更多的内存。

/*确保函数中能创建len个局部引用*/
if(env->EnsureLocalCapacity(len) != 0) {
    ... /*申请len个局部引用的内存空间失败 OutOfMemoryError*/
    return;
}
for(i=0; i < len; i++) {
    jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
    // ... 使用jstr字符串
    /*这里不用释放在for循环中临时创建的局部引用*/
}

另外,除了EnsureLocalCapacity函数可以扩充指定容量的局部引用数量外,我们也可以利用Push/PopLocalFrame函数对创建作用范围层层嵌套的局部引用。例如,我们把上面那段处理字符串数组的代码用Push/PopLocalFrame函数对重写:

#define REF_MAX 16 /*最大局部引用数量*/
for (i = 0; i < len; i++) {
    if (env->PushLocalFrame(REF_MAX) != 0) {
        ... /*内存溢出*/
    }
    jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
    ... /* 使用jstr */
    env->PopLocalFrame(NULL);
}

PushLocalFrame为当前函数中需要用到的局部引用创建了一个引用堆栈,(如果之前调用PushLocalFrame已经创建了Frame,在当前的本地引用栈中仍然是有效的)每遍历一次调用env->GetObjectArrayElement(objArray, i);返回一个局部引用时,JVM会自动将该引用压入当前局部引用栈中。而PopLocalFrame负责销毁栈中所有的引用。这样一来,Push/PopLocalFrame函数对提供了对局部引用生命周期更方便的管理,而不需要时刻关注获取一个引用后,再调用DeleteLocalRef来释放引用。在上面的例子中,如果在处理jstr的过程当中又创建了局部引用,则PopLocalFrame执行时,这些局部引用将全都会被销毁。在调用PopLocalFrame销毁当前frame中的所有引用前,如果第二个参数result不为空,会由result生成一个新的局部引用,再把这个新生成的局部引用存储在上一个frame中。请看下面的示例:

// 函数原型
jobject     (*PopLocalFrame)(JNIEnv*, jobject)

jstring myJstr;
for (i = 0; i < len; i++) {
    if (env->PushLocalFrame(REF_MAX) != 0) {
        ... /*内存溢出*/
    }
    jstring jstr = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
    ... /* 使用jstr */
    if (i == 2) {
       myJstr = jstr;
    }
    myJstr = env->PopLocalFrame(myJstr);  // 销毁局部引用栈前返回指定的引用
}

2.全局引用

全局引用:调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,全局引用必须调用DeleteGlobalRef手动释放env->DeleteGlobalRef(jString)。在调用 DeleteGlobalRef 之前,全局引用保证有效。

static jclass gString;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass jString = env->FindClass("java/lang/String");
    gString = env->NewGlobalRef(jString);
}

请注意,jfieldIDjmethodID 属于不透明类型,不是对象引用,且不应传递给 NewGlobalRef。函数返回的 GetStringUTFCharsGetByteArrayElements 等原始数据指针也不属于对象。(这些指针可以在线程之间传递,并且在匹配的 Release 调用完成之前一直有效。)

3.弱全局引用

弱全局引用:调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef手动释放。env->DeleteWeakGlobalRef(globalClass)

static jclass globalClass;
void Test(JNIEnv* env, jobject obj) {
    jclass localClass = env->FindClass("java/lang/String");
    globalClass = env->NewWeakGlobalRef(localClass);
}

与全局引用类似,弱引用可以跨方法、线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止GC回收它引用的对象。当本地代码中缓存的引用不一定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。所以在使用弱引用时,必须先检查缓存过的弱引用是指向活动的类对象。 弱全局引用通过env->IsSameObject(obj1, obj2) 来判断指向的类对象是否为活动的类对象。

4. 引用比较

给定两个引用(不管是全局、局部还是弱全局引用),我们只需要调用IsSameObject来判断它们两个是否指向相同的对象。例如:env->IsSameObject(obj1, obj2),如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。有一个特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null对象。如果obj是一个局部或全局引用,使用env->IsSameObject(obj, NULL) 或者 obj == NULL 来判断obj是否指向一个null对象即可。

jobject localRef = env->NewObject(xxx_cls,xxx_mid);
jobject wgRef = env->NewWeakGlobalRef(localRef);
...
jboolean isEqual = env->IsSameObject(wgRef, NULL);

在上面的IsSameObject调用中,如果g_obj_ref指向的引用已经被回收,会返回JNI_TRUE,如果g_obj_ref仍然指向一个活动对象,会返回JNI_FALSE

文章评论已关闭!