美文网首页Android-NDK/JNIJNI&NDK
JNI&NDK开发最佳实践(八):JNI局部引用、全局引

JNI&NDK开发最佳实践(八):JNI局部引用、全局引

作者: taoyyyy | 来源:发表于2019-07-12 19:10 被阅读2次

    三种引用的简介及区别

    局部引用

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

    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    jcharArray charArr = (*env)->NewCharArray(env, len);
    jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
    jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj);   // 通过NewLocalRef函数创建
    ...
    

    全局引用

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

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

    弱全局引用

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

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

    引用管理

    引用缓存

    1. 为什么要缓存引用?

    当我们在本地代码中要访问Java对象的字段或调用它们的方法时,本机代码必须调用FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID()。对于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。

    2. 缓存引用的两种方式

    2.1 使用时缓存

    // AccessCache.c
    #include "com_study_jnilearn_AccessCache.h"
    
    JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField
      (JNIEnv *env, jobject obj)
    {
        // 第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用
        static jfieldID fid_str = NULL;
        jclass cls_AccessCache;
        jstring j_str;
        const char *c_str;
        cls_AccessCache = (*env)->GetObjectClass(env, obj); // 获取该对象的Class引用
        if (cls_AccessCache == NULL) {
            return;
        }
    
        // 先判断字段ID之前是否已经缓存过,如果已经缓存过则不进行查找
        if (fid_str == NULL) {
            fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");
    
            // 再次判断是否找到该类的str字段
            if (fid_str == NULL) {
                return;
            }
        }
    
        j_str = (*env)->GetObjectField(env, obj, fid_str);  // 获取字段的值
        c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
        if (c_str == NULL) {
            return; // 内存不够
        }
        printf("In C:\n str = \"%s\"\n", c_str);
        (*env)->ReleaseStringUTFChars(env, j_str, c_str);   // 释放从从JVM新分配字符串的内存空间
    
        // 修改字段的值
        j_str = (*env)->NewStringUTF(env, "12345");
        if (j_str == NULL) {
            return;
        }
        (*env)->SetObjectField(env, obj, fid_str, j_str);
    
        // 释放本地引用
        (*env)->DeleteLocalRef(env,cls_AccessCache);
        (*env)->DeleteLocalRef(env,j_str);
    }
    
    JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
    (JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
    {
        jcharArray elemArray;
        jchar *chars = NULL;
        jstring j_str = NULL;
        static jclass cls_string = NULL;
        static jmethodID cid_string = NULL;
        // 注意:这里缓存局引用的做法是错误,这里做为一个反面教材提醒大家,下面会说到。
        if (cls_string == NULL) {
            cls_string = (*env)->FindClass(env, "java/lang/String");
            if (cls_string == NULL) {
                return NULL;
            }
        }
    
        // 缓存String的构造方法ID
        if (cid_string == NULL) {
            cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
            if (cid_string == NULL) {
                return NULL;
            }
        }
    
        printf("In C array Len: %d\n", len);
        // 创建一个字符数组
        elemArray = (*env)->NewCharArray(env, len);
        if (elemArray == NULL) {
            return NULL;
        }
    
        // 获取数组的指针引用,注意:不能直接将jcharArray作为SetCharArrayRegion函数最后一个参数
        chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL);
        if (chars == NULL) {
            return NULL;
        }
        // 将Java字符数组中的内容复制指定长度到新的字符数组中
        (*env)->SetCharArrayRegion(env, elemArray, 0, len, chars);
    
        // 调用String对象的构造方法,创建一个指定字符数组为内容的String对象
        j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
    
        // 释放本地引用
        (*env)->DeleteLocalRef(env, elemArray);
    
        return j_str;
    }
    

    关键在于利用了C中static关键字在第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用。
    注意:cls_string是一个局部引用,与方法和字段ID不一样,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的。

    2.2 类静态初始化缓存

    package com.study.jnilearn;
    
    public class AccessCache {
    
        public static native void initIDs(); 
    
        public native void nativeMethod();
        public void callback() {
            System.out.println("AccessCache.callback invoked!");
        }
    
        public static void main(String[] args) {
            AccessCache accessCache = new AccessCache();
            accessCache.nativeMethod();
        }
    
        static {
            System.loadLibrary("AccessCache");
            initIDs();
        }
    }
    
    // AccessCache.c
    
    #include "com_study_jnilearn_AccessCache.h"
    
    jmethodID MID_AccessCache_callback;
    
    JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
    (JNIEnv *env, jclass cls)
    {
        printf("initIDs called!!!\n");
        MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V");
    }
    
    JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
    (JNIEnv *env, jobject obj)
    {
        printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n");
        (*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
    }
    

    JVM加载AccessCache.class到内存当中之后,会调用该类的静态初始化代码块,即static代码块,先调用System.loadLibrary加载动态库到JVM中,紧接着调用native方法initIDs,会调用用到本地函数Java_com_study_jnilearn_AccessCache_initIDs,在该函数中获取需要缓存的ID,然后存入全局变量当中。下次需要用到这些ID的时候,直接使用全局变量当中的即可,如18行当中调用Java的callback函数。

    3. static缓存引用的误区

    如果用局部引用+static去缓存引用,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的,应该使用全局引用。

    JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
    (JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
    {
        // ...
        jstring jstr = NULL;
        static jclass cls_string = NULL;
        if (cls_string == NULL) {
            jclass local_cls_string = (*env)->FindClass(env, "java/lang/String");
            if (cls_string == NULL) {
                return NULL;
            }
    
            // 将java.lang.String类的Class引用缓存到全局引用当中
            cls_string = (*env)->NewGlobalRef(env, local_cls_string);
    
            // 删除局部引用
            (*env)->DeleteLocalRef(env, local_cls_string);
    
            // 再次验证全局引用是否创建成功
            if (cls_string == NULL) {
                return NULL;
            }
        }
    
        // ....
        return jstr;
    }
    

    局部引用表溢出

    在Android中局部引用表默认最大容量是512个。这是虚拟机实现的,在程序中应该没办法修改这个数量。所以在一个本地方法中,如果使用了大量的局部引用而没有调用env->DeleteLocalRef及时释放的话,随时都有可能造成程序崩溃的现象。

    // 给数组中每个元素赋值
    for (i = 0; i < count; ++i) {
        memset(buff, 0, sizeof(buff));   // 初始一下缓冲区
        sprintf(buff, c_str_sample,i);
        jstring newStr = (*env)->NewStringUTF(env, buff);
        (*env)->SetObjectArrayElement(env, str_array, i, newStr);
        (*env)->DeleteLocalRef(env,newStr);   // Warning: 这里如果不手动释放局部引用,很有可能造成局部引用表溢出
    }
    

    更多JNI&NDK系列文章,参见:
    JNI&NDK开发最佳实践(一):开篇
    JNI&NDK开发最佳实践(二):CMake实现调用已有C/C++文件中的本地方法
    JNI&NDK开发最佳实践(三):CMake实现调用已有so库中的本地方法
    JNI&NDK开发最佳实践(四):JNI数据类型及与Java数据类型的映射关系
    JNI&NDK开发最佳实践(五):本地方法的静态注册与动态注册
    JNI&NDK开发最佳实践(六):JNI实现本地方法时的数据类型转换
    JNI&NDK开发最佳实践(七):JNI之本地方法与java互调
    JNI&NDK开发最佳实践(八):JNI局部引用、全局引用和弱全局引用
    JNI&NDK开发最佳实践(九):调试篇

    相关文章

      网友评论

        本文标题:JNI&NDK开发最佳实践(八):JNI局部引用、全局引

        本文链接:https://www.haomeiwen.com/subject/sqoykctx.html