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

原文出处:http://www.ccbu.cc/index.php/android/android-jni-ref.html

在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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容