对象引用对于非静态的字段 方法或属性_Android NDK(二) JNI缓存策略与引用管理

通过Android NDK(一) 理解JNI技术全面地理解JNI的技术背景及原理之后,我们来看看 JNI 在 Android NDK 开发中的最佳实践。官方已经有比较详细的教程了,文末有参考链接,本文是基于自身理解做进一步的总结。

缓存策略

在 JNI 编程中我们会使用GetFieldIDGetMethodID获取Java层对象的属性和方法ID。由于属性和方法可能继承自父类,JVM 查找时需要向上遍历父类,这两个方法会比较耗时。因此,对应 JNI 调用频繁的情况,善用缓存策略可以提升应用性能。缓存策略一般有两种,分别是使用时缓存和类初始化时缓存。

使用时缓存

使用局部静态变量缓存,第一次调用后,字段或方法ID被缓存起来。

//native-lib.cpp

类初始化时缓存

类加载的初始化阶段,会调用到此类的静态初始化代码块,可以在这个过程缓存字段或方法ID。


//JNITest.kt
init {
System.loadLibrary("native-lib")
initIDs()
}

//native-lib.cpp
static jmethodID method_cache = nullptr;

JNIEXPORT void JNICALLJava_com_devnan_ndk_JNITest_initIDs(JNIEnv *env, jobject thiz) {
jclass cls = env->GetObjectClass(thiz);
method_cache = env->GetMethodID(cls, "javaMethod", "()Ljava/lang/String;");
LOGD("initIDs");
}

JNIEXPORT void JNICALLJava_com_devnan_ndk_JNITest_callJavaMethod(JNIEnv *env, jobject thiz) {
jstring str = (jstring) (env->CallObjectMethod(thiz, method_cache));
const char *str_value = (env)->GetStringUTFChars(str, nullptr);
LOGD("callJavaMethod: %s" , str_value);
}

小结

1、当不能控制要缓存的属性或方法所在的类源码时,"使用时缓存"策略比较合理;
2、"使用时缓存"策略每次使用都要检查,而且在多线程情况下可能出现重复初始化。应该尽量在类的静态初始化代码段中缓存属性或者方法ID。

引用管理

在 Java 中有四种引用类型,分别是强引用、软引用、弱引用以及虚引用。不同的引用类型决定了一个 Java 对象如何被 GC 管理,即内存是否可以被回收以及何时回收。Java的引用机制只能在Java层起作用,如果需要在 JNI 层使用,比如告知 JVM 哪些对象不能被GC回收,就需要制定一套新的规范。在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。

局部引用

绝大多数的 JNI 函数(FindClass、NewObject、GetObjectClass 和 NewCharArray 等)返回的引用是局部引用,其生命周期从创建引用开始,到 JNI 方法返回到 Java 上下文时结束。目的是保证了在其生命周期内所引用的对象不会被GC回收。因此, 当局部引用不再使用时,如果依然引用了对象使其不能被GC回收,就会造成内存泄漏。一般当局部引用不再使用时,我们可以主动调用DeleteLocalRef来提前释放它所引用的对象,这样就可以及时被 GC 回收,否则只能等到JNI方法执行完毕返回。

在循环或大对象引用的情况下,要注意及时主动释放局部引用。

JNIEXPORT void JNICALLJava_com_devnan_ndk_JNITest_localRef(JNIEnv *env, jobject thiz, jint count) {

简单画了一个图来表示上面的代码,如下,局部引用表用来存放本次JNI方法执行过程中创建的局部引用,也就是每当在 Native 环境引用到一个 Java 对象就生成一个局部引用。Native 访问 Java 对象就是通过局部引用表的映射。在上面代码中,循环生成String对象,需要及时释放局部引用,不然有越来越多的局部引用被创建,可能造成局部引用表溢出,导致崩溃。另一方面,及时的释放也解决了 Java heap 短时的内存泄漏,避免造成 OOM。

f7ea83f468ab334460414234358c6187.png

还有,上面代码中调用FindClass创建了局部引用指向 Java heap 的 String Class 对象,而g_str是个静态局部变量,生命周期和应用的生命周期一致,当方法执行完毕返回,局部引用表会被回收,此时g_str指向的内存被回收,g_str成为野指针,下次调用会因为访问无效内存而崩溃。因此不应该去缓存局部引用,如果要缓存,可以使用全局引用。

其实,除了DeleteLocalRef,JNI 还提供了一些函数来方便管理局部引用的生命周期。比如可以使用PushLocalFrame/PopLocalFrame函数来创建一个指定大小的局部引用区域,在函数入口的时候调用PushLocalFrame,并在所有退出的case调用PopLocalFrame。在局部引用创建较多,并且此时还没有返回到 Java 上下文环境的情况下,建议使用此方式来管理局部引用。举个例子,AndroidRuntime中使用静态方法startReg进行JNI注册时,就是使用了PushLocalFrame/PopLocalFrame函数。

/*
* Register android native functions with the VM.
*/

全局引用

与局部引用不同,全局引用需要指定JNI函数NewGlobalRef来创建,并通过DeleteGlobalRef释放。全局引用的生命周期明显比局部引用要长,需要被手动释放才结束。在其生命周期内,其引用的对象不会被 GC 回收。全局引用可以跨方法、跨线程使用。

使用全局引用缓存FindClass的返回,而不是直接缓存局部引用。

jclass g_cls_string;

JNIEXPORT void JNICALLJava_com_devnan_ndk_JNITest_globalRef(JNIEnv *env, jobject thiz) {
if (g_cls_string == nullptr) {
jclass cls_string = env->FindClass("java/lang/String");
if (cls_string == nullptr) {
return;
}
g_cls_string = (jclass) env->NewGlobalRef(cls_string);
LOGD("globalRef");
}
}

弱全局引用

弱全局引用对于全局引用,类似于 Java 中的弱引用对于强引用。弱全局引用使用NewWeakGlobalRef创建,使用DeleteGlobalWeakRef释放。在引用生命周期内,弱全局引用不会阻止其引用的对象被GC回收,因此在使用该引用时,需要使用IsSameObject来判断非空的弱全局引用是否指向一个有效的对象。

jclass g_weak_cls_string;
JNIEXPORT void JNICALLJava_com_devnan_ndk_JNITest_weakGlobalRef(JNIEnv *env, jobject thiz) {
if (g_weak_cls_string == nullptr) {
jclass cls_string = env->FindClass("java/lang/String");
if (cls_string == nullptr) {
return;
}
g_weak_cls_string = (jclass) env->NewWeakGlobalRef(cls_string);
if (g_weak_cls_string == nullptr) {
return;
}
LOGD("weakGlobalRef");
}
//判断所引用的对象是否被回收
if (env->IsSameObject(g_weak_cls_string, nullptr)) {
LOGD("weakGlobalRef is recycled");
} else {
LOGD("weakGlobalRef is alive");
}
}

小结

1、合理管理 JNI 引用可以提升应用的可靠性,优化内存占用;
2、局部引用和全局引用的生命周期不同,局部引用会被自动释放,而全局引用和弱全局引用需要手动释放。局部引用和全局引用会阻止所引用的对象被GC回收,而全局弱引用不会;
3、当创建大量局部引用时,可以使用PushLocalFrame/PopLocalFrame函数以方便管理其生命周期。

参考

  1. The Java Native Interface: Programmer's Guide and Specification
  2. Java Native Interface 6.0 Specification
  3. https://www.ibm.com/developerworks/cn/java/j-lo-jnileak/index.html
337203956d8ec425e404be0b2f347cc1.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值