Android-JNI开发系列《五》局部引用&全局引用&全局弱引用&缓存策略 - 新闻资讯 - 云南小程序开发|云南软件开发|云南网站建设-昆明葵宇信息科技有限公司

159-8711-8523

云南网建设/小程序开发/软件开发

知识

不管是网站,软件还是小程序,都要直接或间接能为您产生价值,我们在追求其视觉表现的同时,更侧重于功能的便捷,营销的便利,运营的高效,让网站成为营销工具,让软件能切实提升企业内部管理水平和效率。优秀的程序为后期升级提供便捷的支持!

您当前位置>首页 » 新闻资讯 » 技术分享 >

Android-JNI开发系列《五》局部引用&全局引用&全局弱引用&缓存策略

发表时间:2020-10-17

发布人:葵宇科技

浏览次数:30

人间观察

好像什么都来得及,又好像什么都来不及。

本篇文章主要介绍在jni开发中常见的三种引用的使用方法和注意事项以及jni和java交互的缓存策略。

我们知道Java是一门纯面象对象的语言,除了基本数据类型外,其它任何类型所创建的对象的内存都存在堆空间中。内存由JVM 的GC(Garbage Collection)垃圾回收进行管理。

但是对于c,c++中以及用c/c++编写的jni来说同样需要手动管理和处理内存,特别是引用类型的对象。malloc,realloc,free ,delete ,不像java有jvm对每个进程内存的限制,特别是Android移动端使用不当给你oom好不啦。而在c++/c只要你想要多大的来随便搞(只要系统内存充足)但是需要释放。各有各的优势。

在jni中分为局部引用,全局引用,全局弱引用,个人认为有点类似于java中
局部引用,强引用,软引用SoftReference。在使用介绍之前我们先看一下jni中的基本类型和引用类型有哪些以及对应关系。

jni数据类型

基本数据类型

java与Native映射关系如下表所示:

Java类型Native 类型Descriptionbooleanjbooleanunsigned 8 bitsbytejbytesigned 8 bitscharjcharunsigned 16 bitsshortjshortsigned 16 bitsintjint signed32 bitslong jlongsigned64 bitsfloatjfloat32 bitsdoublejdouble64 bitsvoidvoidnot applicable

引用数据类型

外面的为jni中的,括号中的java中的。

  • jobject
    • jclass (java.lang.Class objects)
    • jstring (java.lang.String objects)
    • jarray (arrays)
      • jobjectArray (object arrays)
      • jbooleanArray (boolean arrays)
      • jbyteArray (byte arrays)
      • jcharArray (char arrays)
      • jshortArray (short arrays)
      • jintArray (int arrays)
      • jlongArray (long arrays)
      • jfloatArray (float arrays)
      • jdoubleArray (double arrays)
  • jthrowable (java.lang.Throwable objects)

上面的层次中的jni的引用类型代表了继承关系,jbooleanArray继承jarray,jarray继承jobject,最终都继承jobject。

局部引用

通过调用jni的一些方法比如FindClassNewCharArrayNewStringUTF等只要是返回上面介绍的jni的引用类型都属于局部引用,局部引用的生命周期只在方法中效,不能垮线程跨方法使用,函数退出后局部引用所引用的对象会被JVM自动释放,或显示调用DeleteLocalRef释放。局部引用的也可以通过(*env)->NewLocalRef(env,local_ref)方法创建,一般不常用。

如下示例:

// jni_ref.cpp
// 在jni中调用java String类构造返回String
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jnilocalRef(JNIEnv *env, jobject instance) {

    // 局部引用
    jclass local_j_cls = env->FindClass("java/lang/String");

    // 调用public String(char[] value); 构造方法。 为了演示更多的局部引用
    jmethodID j_mid = env->GetMethodID(local_j_cls, "<init>", "([C)V");
    // 局部引用
    jcharArray local_j_charArr = env->NewCharArray(8);
    // 局部引用
    jstring local_str = env->NewStringUTF("LocalRef");
    const jchar *j_char = env->GetStringChars(local_str, nullptr);

    env->SetCharArrayRegion(local_j_charArr, 0, 8, j_char);

    jstring j_str = (jstring) env->NewObject(local_j_cls, j_mid, local_j_charArr);

    // 释放局部引用,也可以不用调用在方法结束后jvm会自动回收,最好有良好的编码习惯
    env->DeleteLocalRef(local_j_cls);
    env->DeleteLocalRef(local_str);
    env->DeleteLocalRef(local_j_charArr);

    // 也可以通过NewLocalRef函数创建 (*env)->NewLocalRef(env,local_ref);这个方法一般很少用。
    // 函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放。(*env)->DeleteLocalRef(env,local_ref)

    // ReleaseStringChars和GetStringChars对应
    env->ReleaseStringChars(j_str, j_char);

    return j_str;
}

例子中的local_j_cls,local_j_charArr,local_j_charArr,j_str 都是局部引用类型。最后调用了DeleteLocalRef来释放。
有同学问了,既然局部引用不用手动释放,可不可以不用调用DeleteLocalRef方法。
咦,你这个小可爱,好问题哦!
好问题哦

我网上搜索了下,大部分的文章说了下会有限制。超过512个局部引用(为什么是这个数字,一看就是一个有情怀的程序员)会造成局部引用表溢出。我还是想测试一下如下

// jni_ref.cpp
    LOG_D("localRefOverflow start");
    for (int i = 0; i < count; i++) {
        jclass local_j_cls = env->FindClass("java/util/ArrayList");
        // env->DeleteLocalRef(local_j_cls);
    }
    LOG_D("localRefOverflow end");

count =513,没有报错,打印了localRefOverflow end

count =2000,没有报错,打印了localRefOverflow end

count =10000,没有报错,打印了localRefOverflow end

count =10 0000,没有报错,打印了localRefOverflow end

count =100 0000,没有报错,打印了localRefOverflow end

我靠,WTF? 直接for循环900w次。异常出现了。

2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] JNI ERROR (app bug): local reference table overflow (max=8388608)
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] local reference table dump:
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]   Last 10 entries (of 8388608):
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388607: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388606: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388605: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388604: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388603: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388602: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388601: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388600: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388599: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388598: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]   Summary:
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]     8388604 of java.lang.Class (3 unique instances)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]         3 of java.lang.String (3 unique instances)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]         1 of java.lang.String[] (3 elements)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273]  Resizing failed: Requested size exceeds maximum: 16777216

8388608 ,可以猜测是不同的Android 版本导致,Android经常这样不同的API或者功能在不同的版本上表现不一样。 而我我用的Android 8.1的系统。为什么512没有报错了。
Android 8.0 之前局部引用表的上限是512个引用,Android 8.0后局部引用表上限提升到了8388608个引用。大家想一探究竟的话可以在如下Android 底层代码中看一下。

需要翻墙
有关底层源码

看源码的同时我们也看到了比如FindClass 等方法,在最后方法的最后都有类似添加到局部引用表里的代码,也就是说需要我们手动删除局部引用。

 static jclass FindClass(JNIEnv* env, const char* name) {
    CHECK_NON_NULL_ARGUMENT(name);
    Runtime* runtime = Runtime::Current();
    ClassLinker* class_linker = runtime->GetClassLinker();
    std::string descriptor(NormalizeJniClassDescriptor(name));
    ScopedObjectAccess soa(env);
    mirror::Class* c = nullptr;
    if (runtime->IsStarted()) {
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader(soa)));
      c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
    } else {
      c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
    }
    return soa.AddLocalReference<jclass>(c);
  }

综上可以看出并不是局部引用不用调用DeleteLocalRef来释放。而是建议调用一下。如果你的jni方法很简单&与java交互很少也可以不调用。但是如下的一些情况需要手动显示的调用,为了防止内存溢出和局部引用表溢出。

  1. 如上我们模拟的情况,在for循环里或者其它操作类似频繁创建局部引用的需要释放
  2. 遍历数组产生的局部引用,用完后要删除。

全局引用

通过调用jobject NewGlobalRef(jobject obj)基于引用来创建,参数是jobject类型。它可以跨方法、跨线程使用。JVM不会自动释放它,必须显示调用DeleteGlobalRef手动释放void DeleteGlobalRef(jobject globalRef)

如下使用示例:
在jni中调用java String类构造返回String

// jni_ref.cpp
static jclass g_j_cls;  // 加static前缀 只对本源文件可见,对其它源文件隐藏
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jniGlobalRef(JNIEnv *env, jobject instance) {

    if (g_j_cls == nullptr) {
        jclass local_j_cls = env->FindClass("java/lang/String");
        // 将local_j_cls局部引用改为全局引用
        g_j_cls = (jclass) env->NewGlobalRef(local_j_cls);
    } else {
        LOG_D("g_j_cls else");
    }

    // 调用public String(String value); 构造
    jmethodID j_mid = env->GetMethodID(g_j_cls, "<init>", "(Ljava/lang/String;)V");

    jstring str = env->NewStringUTF("GlobalRef");
    jstring j_str = (jstring) env->NewObject(g_j_cls, j_mid, str);
    return j_str;
}

extern "C" JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_delGlobalRef(JNIEnv *env, jobject instance) {
    if (g_j_cls != nullptr) {
        LOG_D("DeleteGlobalRef");
        // 释放某个全局引用
        env->DeleteGlobalRef(g_j_cls);
    }
}

java调用

    public native String jniGlobalRef();
    public native void delGlobalRef();
    
    String ret1 = jniRef.jniGlobalRef();
    Log.e(TAG, "jniGlobalRef=" + ret1);
    String ret2 = jniRef.jniGlobalRef();
    Log.e(TAG, "jniGlobalRef=" + ret2);
    jniRef.delGlobalRef();

g_j_cls就是一个全局引用,然后我们多次调用下jniRef.jniGlobalRef方法打印如下:

2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp E/JNI: jniGlobalRef=GlobalRef
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp D/JNI: g_j_cls else
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp E/JNI: jniGlobalRef=GlobalRef
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp D/JNI: DeleteGlobalRef

说明全局引用可以起到缓存的效果,为什么要做这个测验呢? 因为频繁调用类似JNI接口FindClass查找java中Class引用时是比较耗性能的,特别是在有交互频繁的JNI的app中。

弱全局引用

这个有点类似于java的软引用SoftReference,jvm在内存不足的时候会释放它。通过调用jweak NewWeakGlobalRef(jobject obj)来创建一个弱全局引用,释放调用void DeleteWeakGlobalRef(jweak obj)jweaktypedef _jobject* jweak;
_jobject指针的别名。

如下使用示例,和全局引用一样把全局引用的方法改为弱全局引用的方法即可。

// jni_ref.cpp
static jclass g_w_j_cls;
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jniWeakGlobalRef(JNIEnv *env, jobject instance) {

    if (g_w_j_cls == nullptr) {
        jclass local_j_cls = env->FindClass("java/lang/String");
        // 将local_j_clss局部引用改为弱全局引用
        g_w_j_cls = (jclass) env->NewWeakGlobalRef(local_j_cls);
    } else {
        LOG_D("g_w_j_cls else");
    }

    jmethodID j_mid = env->GetMethodID(g_w_j_cls, "<init>", "(Ljava/lang/String;)V");

    // 使用弱引用时,必须先检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC的类对象
    // 检查弱引用是否活动,即引用的比较IsSameObject
    // 如果g_w_j_cls指向的引用已经被回收,会返回JNI_TRUE
    // 如果仍然指向一个活动对象,会返回JNI_FALSE
    jboolean isGC = env->IsSameObject(g_w_j_cls, nullptr);
    if (isGC) {
        LOG_D("weak reference has been gc");
        return env->NewStringUTF("weak reference has been gc");
    } else {
        jstring str = env->NewStringUTF("WeakGlobalRef");
        jstring j_str = (jstring) env->NewObject(g_w_j_cls, j_mid, str);
        return j_str;
    }
}

extern "C" JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_delWeakGlobalRef(JNIEnv *env, jobject instance) {
    if (g_w_j_cls != nullptr) {
        // 调用DeleteWeakGlobalRef来释放它,如果不手动调用这个函数来释放所指向的对象,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。
        LOG_D("DeleteWeakGlobalRef");
        env->DeleteWeakGlobalRef(g_w_j_cls);
    }
}

java调用

        String ret3 = jniRef.jniWeakGlobalRef();
        Log.e(TAG, "jniWeakGlobalRef=" + ret3);
        String ret4 = jniRef.jniWeakGlobalRef();
        Log.e(TAG, "jniWeakGlobalRef=" + ret4);
        jniRef.delWeakGlobalRef();

g_w_j_cls就是一个弱全局引用,然后我们多次调用下jniRef.jniWeakGlobalRef方法打印如下:

2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp E/JNI: jniWeakGlobalRef=WeakGlobalRef
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp D/JNI: g_w_j_cls else
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp E/JNI: jniWeakGlobalRef=WeakGlobalRef
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp D/JNI: DeleteWeakGlobalRef

和全局引用一样可以起到缓存的效果。
刚才我们说了就是弱全局引用在内存不足的时候会被jvm回收,怎么判断它被回收了,判null ,没错!当被回收了会为null。所以我们在使用弱全局引用的时候频道弱全局引用是否还存在。怎么判断呢?使用引用比较 。

引用比较

在jni中提供了 jboolean IsSameObject(jobject ref1, jobject ref2)方法。如果ref1和ref2指向同个对象则返回JNI_TRUE,否则返回JNI_FALSE

    jclass local_j_cls_1 = env->FindClass("java/util/ArrayList");
    jclass local_j_cls_2 = env->FindClass("java/util/ArrayList");
    jboolean same1 = env->IsSameObject(local_j_cls_1, local_j_cls_2);
    LOG_D("%d",same1);
    jboolean same2= env->IsSameObject(local_j_cls_1, nullptr);
    LOG_D("%d",same2);

输出 1和0

缓存策略

当我们在本地代码方法中通过FindClass查找Class、GetMethodID查找方法、GetFieldID获取类的字段ID和GetFieldValue获取字段的时候是需要jvm来做很多工作的,可能这个字段ID或者方法是在超类中继承而来的,那jvm可能还需要层次遍历。而这些负责和jni交互java中的类的全路径,字段,方法一般是不会修改了,是固定的。这也是为什么我们在做android混淆打包的时候需要keep这些类,因为这些一般不会变,不能变,变了后jni中会找不到了具体的类,字段,方法了。既然打包后不会变我们是可以进行缓存策略来处理。

另外至于效率提高多少,没有验证,不过不重要,如果是频繁这种查找一般会采用缓存,只查找一次或者在程序初始化的时候提前查找。

对于这类情况的缓存分为基本数据类型缓存和引用缓存。

基本数据类型缓存

基本数据类型的缓存在c,c++中可以借助关键字static处理。
学过c,c++的都知道

  1. static局部变量只初始化一次,下一次依据上一次结果值
  2. static全局变量只初使化一次,防止在其他文件中被引用
  3. 加static函数的函数为内部函数,只能在本源文件中使用, 和普通函数的作用域不同
static jclass g_j_cls_cache;
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_refCache(JNIEnv *env, jobject instance) {
    if (g_j_cls_cache == nullptr) {
        jclass local_j_cls = env->FindClass("java/lang/String");
        // 将local_j_cls局部引用改为全局引用
        g_j_cls_cache = (jclass) env->NewGlobalRef(local_j_cls);
    } else {
        LOG_D("g_j_cls_cache use cache");
    }

    // 调用public String(String value); 构造
    static jmethodID j_mid;
    if (j_mid == nullptr) {
        j_mid = env->GetMethodID(g_j_cls_cache, "<init>", "(Ljava/lang/String;)V");
    } else {
        LOG_D("j_mid use cache");
    }

    jstring str = env->NewStringUTF("refCache");
    jstring j_str = (jstring) env->NewObject(g_j_cls_cache, j_mid, str);
    return j_str;
}

java调用

        String ret5 = jniRef.refCache();
        Log.e(TAG, "refCache=" + ret5);
        String ret6 = jniRef.refCache();
        Log.e(TAG, "refCache=" + ret6);
        jniRef.delRefCache();

local_j_cls局部引用变为全局引用,j_mid变量改为static
输出:

10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp E/JNI: refCache=refCache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp D/JNI: g_j_cls_cache use cache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp D/JNI: j_mid use cache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp E/JNI: refCache=refCache

有人问local_j_cls局部引用可以加static吗?不用全局引用/全局弱应用? 可以加static,但是不能起到缓存的作用。因为上文说了局部引用在函数结束后会被jvm回收了,不然再次使用回到非法内存访问导致应用crash,所以正确的做法如上用全局引用/全局弱应用。

引用类型的缓存

可以借助上面的全局引用或者弱全局引用,弱全局引用记得在使用前判断下是否被回收了IsSameObject,最后记得释放 DeleteGlobalRef ,DeleteWeakGlobalRef

最后源代码:https://github.com/ta893115871/JNIAPP

相关案例查看更多