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

人间观察

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

本篇文章主要介绍在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 类型 Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void not 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

猜你喜欢

转载自blog.csdn.net/ta893115871/article/details/109135424