美文网首页
JNI代码实践

JNI代码实践

作者: 云佾风徽 | 来源:发表于2018-12-17 23:49 被阅读0次

    JNI代码实践

    [TOC]

    说明

    关于jni代码的cmake构建脚本,kotlin如何声明和使用native方法,jni层如何进行socket通讯,jni层如何进行多线程操作,请参见我的另一篇文章JNI入门

    1. reference的练习与观察: TestNativeReference
    2. jni.h声明的api操作java类与对象+动态注册jni接口方法: TestNativeInterface

    Local Reference / Global Reference / Global Weak Reference

    Local Reference 的生命期

    native method 的执行期(从 Java 程序切换到 native code 环境时开始创建,或者在 native method 执行时调用 JNI function 创建),在 native method 执行完毕切换回 Java 程序时,所有 JNI Local Reference 被删除,生命期结束(调用 JNI function 可以提前结束其生命期)。

    local ref 创建的时机

    1. java层调用native方法,进入native上下文环境会初始化一些jobject及子类的local ref
    2. native方法执行期间,调用了jni.h中的jni接口方法创建出的对象比如NewObject,会产生local ref

    local ref销毁的时机

    1. java层调用的native方法执行完毕,return返回,切换回java层,所有local ref都销毁
    2. 调用jni.h中的jni接口可以提前销毁local ref ,例如DeleteLocalRef

    Local Reference tabel

    java环境下,调用声明的jni方法,从当前线程切换到native code上下文环境。

    此时JVM会在当前线程的本地方法栈(native method stack,线程独立的内存区域,线程共享的是gc堆和方法区)分配一块内存,创建发起的jni方法的生命周期内的Local Reference tabel。

    用于存放本次native code执行中创建的所有Local Reference,每当在native code中创建一个新的jobject及其子类对象,就会往该表中添加一条引用记录。

    image

    引用数量上限

    每次进入native创建的local ref表有引用数量的上限,如果超过上限则会异常崩溃。

    • 各种指南上说jni保证可以使用16个local ref

      Programmers are required to "not excessively allocate" local references. In practical terms this means that if you're creating large numbers of local references, perhaps while running through an array of objects, you should free them manually with DeleteLocalRef instead of letting JNI do it for you. The implementation is only required to reserve slots for 16 local references, so if you need more than that you should either delete as you go or use EnsureLocalCapacity/PushLocalFrame to reserve more.

    • 实际执行代码发现可以持有512个引用。

    extern "C"
    JNIEXPORT void JNICALL
    Java_cn_rexih_android_testnativeinterface_MainActivity_testLocalRefOverflow(JNIEnv *env, jobject instance) {
    
        char a[5];
        // 进入native环境时local ref table就会存在几条引用,所以还没到512时就会溢出
        for (int i = 0; i < 512; ++i) {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "before:%d", i);
            sprintf(a, "%d", i);
            jstring pJstring = env->NewStringUTF(a);
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "after:%d", i);
        }
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "test finish");
    
    }
    

    dalvik虚拟机执行时崩溃,日志信息

    JNI ERROR (app bug): local reference table overflow (max=512)
    JNI local reference table (0xb98680a0) dump:
      Last 10 entries (of 512):
          511: 0xa4fcaea0 java.lang.String "504"
          510: 0xa4fcae68 java.lang.String "503"
          509: 0xa4fcae30 java.lang.String "502"
          508: 0xa4fcadf8 java.lang.String "501"
          507: 0xa4fcadc0 java.lang.String "500"
          506: 0xa4fcad88 java.lang.String "499"
          505: 0xa4fcad50 java.lang.String "498"
          504: 0xa4fcad18 java.lang.String "497"
          503: 0xa4fcace0 java.lang.String "496"
          502: 0xa4fcaca8 java.lang.String "495"
      Summary:
            3 of java.lang.Class (3 unique instances)
          507 of java.lang.String (507 unique instances)
            1 of java.lang.String[] (2 elements)
            1 of cn.rexih.android.testnativeinterface.MainActivity
    Failed adding to JNI local ref table (has 512 entries)
    

    art虚拟机崩溃日志

        --------- beginning of crash
    2018-12-09 15:55:58.062 23487-23487/? A/libc: stack corruption detected (-fstack-protector)
    2018-12-09 15:55:58.062 23487-23487/? A/libc: Fatal signal 6 (SIGABRT), code -6 (SI_TKILL) in tid 23487 (nativeinterface), pid 23487 (nativeinterface)
    

    dump local reference

    代码强制进行 reference table dump

    extern "C" JNIEXPORT void JNICALL
    Java_cn_rexih_android_testnativeinterface_MainActivity_testInitLocalRef(JNIEnv *env, jobject instance, jobject entity) {
    
        // 标记
        jstring pMark = env->NewStringUTF("I'm a mark");
    
        // 强制进行 reference table dump
        // 查找了一次VMDebug类,会添加到local ref table中
        jclass vm_class = env->FindClass("dalvik/system/VMDebug");
        jmethodID dump_mid = env->GetStaticMethodID(vm_class, "dumpReferenceTables", "()V");
        env->CallStaticVoidMethod(vm_class, dump_mid);
    
    }
    

    参见Android JNI local reference table, dump current state

    虽然有的post说art不能用dalvik.system.VMDebug,但实测在9.0的虚拟机上仍然可以调用

    dalvik的local reference表记录(4.4设备)

    dalvik虚拟机,在从java环境调用jni方法进入native环境时,会将

    1. 入参的jobject或者jclass(表示jni方法所在实例/类)

    2. 其他入参对应的jobject参数

    添加到本次的local reference表中

    --- reference table dump ---
    JNI local reference table (0xb986f120) dump:
      Last 10 entries (of 10):
            9: 0xa4c881a8 java.lang.Class<dalvik.system.VMDebug>
            8: 0xa4fae008 java.lang.String "I'm a mark"
            7: 0xa4fadfe8 cn.rexih.android.testnativeinterface.entity.Service
            6: 0xa4f70f88 cn.rexih.android.testnativeinterface.MainActivity
            5: 0xa4ce59f0 java.lang.Class<com.android.internal.os.ZygoteInit>
            4: 0xa4cffaf0 java.lang.String "start-system-ser... (19 chars)
            3: 0xa4cffa78 java.lang.String "com.android.inte... (34 chars)
            2: 0xa4cffa60 java.lang.String[] (2 elements)
            1: 0xa4c840e0 java.lang.Class<java.lang.String>
            0: 0xa4c831e8 java.lang.Class<java.lang.Class>
      Summary:
            4 of java.lang.Class (4 unique instances)
            3 of java.lang.String (3 unique instances)
            1 of java.lang.String[] (2 elements)
            1 of cn.rexih.android.testnativeinterface.MainActivity
            1 of cn.rexih.android.testnativeinterface.entity.Service
    JNI global reference table (0xb9868e10) dump:
      Last 10 entries (of 258):
          // ...
    

    art的local reference表记录(9.0设备)

    local ref表的记录与4.4版本dalvik的虚拟机不同,不会把表示jni方法所在实例/类的对象,和其他入参对象对应的jobject类添加到本次的local reference表中

    Accessing hidden method Ldalvik/system/VMDebug;->dumpReferenceTables()V (light greylist, JNI)
    --- reference table dump ---
    local reference table dump:
      Last 8 entries (of 8):
            7: 0x6fcf8db0 java.lang.Class<dalvik.system.VMDebug>
            6: 0x12c72160 java.lang.String "I'm a mark"
            5: 0x7000ed68 java.lang.Class<com.android.internal.os.ZygoteInit>
            4: 0x74314fa8 java.lang.String "--abi-list=x86"
            3: 0x74314f80 java.lang.String "start-system-ser... (19 chars)
            2: 0x7430d4c8 java.lang.String "com.android.inte... (34 chars)
            1: 0x7430d290 java.lang.String[] (3 elements)
            0: 0x6fadbe58 java.lang.Class<java.lang.String>
      Summary:
            4 of java.lang.String (4 unique instances)
            3 of java.lang.Class (3 unique instances)
            1 of java.lang.String[] (3 elements)
    monitors reference table dump:
      (empty)
    global reference table dump:
      Last 10 entries (of 597):
          // ...
    

    参考资料

    IBM J2N

    EnsureLocalCapacity

    有post说可以使用EnsureLocalCapacity避免溢出,通过实际操作来理解:

    extern "C"
    JNIEXPORT void JNICALL
    Java_cn_rexih_android_testnativeinterface_MainActivity_testEnsureLocalCapacity(JNIEnv *env, jobject instance) {
    
        char a[5];
        int capacity = 516;
        // 已知在dalvik里local ref 可以有512,通过不断尝试查找可用的上限
        for (; capacity > 0 ; --capacity) {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "current try alloc :%d", capacity);
            if(0 > env->EnsureLocalCapacity(capacity)){
                // 内存分配失败, 调用ExceptionOccurred也会返回一个jobject占用local ref table,须释放
                jthrowable pJthrowable = env->ExceptionOccurred();
                env->ExceptionDescribe();
                env->ExceptionClear();
                env->DeleteLocalRef(pJthrowable);
            } else {
                __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "success alloc :%d", capacity);
                break;
            }
        }
        env->CallStaticVoidMethod(g_vm_class, g_dump_mid);
        for (int i = 0; i < capacity; ++i) {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "before:%d", i);
            sprintf(a, "%d", i);
            jstring pJstring = env->NewStringUTF(a);
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "after:%d", i);
        }
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "test finish");
        env->CallStaticVoidMethod(g_vm_class, g_dump_mid);
    }
    

    运行结果:

    I/JNI_TEST: current try alloc :506
    W/System.err: java.lang.OutOfMemoryError: can't ensure local reference capacity
    // ...
    I/JNI_TEST: current try alloc :505
    I/JNI_TEST: success alloc :505
    I/dalvikvm: --- reference table dump ---
    W/dalvikvm: JNI local reference table (0xb970b250) dump:
    W/dalvikvm:   Last 7 entries (of 7):
    W/dalvikvm:         6: 0xa4f73d40 cn.rexih.android.testnativeinterface.MainActivity
    W/dalvikvm:         5: 0xa4ce59f0 java.lang.Class<com.android.internal.os.ZygoteInit>
    W/dalvikvm:         4: 0xa4cffaf0 java.lang.String "start-system-ser... (19 chars)
    W/dalvikvm:         3: 0xa4cffa78 java.lang.String "com.android.inte... (34 chars)
    W/dalvikvm:         2: 0xa4cffa60 java.lang.String[] (2 elements)
    W/dalvikvm:         1: 0xa4c840e0 java.lang.Class<java.lang.String>
    W/dalvikvm:         0: 0xa4c831e8 java.lang.Class<java.lang.Class>
    W/dalvikvm:   Summary:
    W/dalvikvm:         3 of java.lang.Class (3 unique instances)
    W/dalvikvm:         2 of java.lang.String (2 unique instances)
    W/dalvikvm:         1 of java.lang.String[] (2 elements)
    W/dalvikvm:         1 of cn.rexih.android.testnativeinterface.MainActivity
    W/dalvikvm: JNI global reference table (0xb9865d40) dump:
    W/dalvikvm:   Last 10 entries (of 261):
    // ...
    

    从测试结果可知:

    1. 使用EnsureLocalCapacity也不能打破jvm的Local ref引用数量上限

    2. EnsureLocalCapacity主要作用是检查即将创建的Local ref是否会超过上限

    3. 从测试中可以得到如下算式:

      传入EnsureLocalCapacity的最大capacity =

      JVM的Local ref引用数量上限 - 当前已存在的Local ref数量

    PushLocalFrame/PopLocalFrame

    • 其作用是保证PushLocalFrame与PopLocalFrame之间的Local ref,在调用PopLocalFrame之后被及时清理掉。
    • PushLocalFrame与EnsureLocalCapacity一样,入参传入的capacity也受限于JVM的Local ref引用数量上限,以及当前已存在的Local ref数量。
    • 如果需要PushLocalFrame与PopLocalFrame之间的某个Local ref在PopLocalFrame之后保留下来,可以将该Local ref作为PopLocalFrame的入参,PopLocalFrame会将PushLocalFrame与PopLocalFrame之间的引用删除后,将此Local Ref转换成其区间之外的(前一帧的)新的Local Ref。
    extern "C"
    JNIEXPORT void JNICALL
    Java_cn_rexih_android_testnativeinterface_MainActivity_testPushLocalFrame(JNIEnv *env, jobject instance) {
    
        char a[5];
        int capacity = 516;
        for (; capacity > 0; --capacity) {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "current try alloc :%d", capacity);
            if (0 > env->PushLocalFrame(capacity)) {
                env->ExceptionClear();
            } else {
                __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "success alloc :%d", capacity);
                break;
            }
        }
        env->CallStaticVoidMethod(g_vm_class, g_dump_mid);
        jobject lastVal;
        for (int i = 0; i < capacity; ++i) {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "before:%d", i + 1);
            sprintf(a, "%d", i + 1);
            lastVal = env->NewStringUTF(a);
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "after:%d:addr:0x%x", i + 1, lastVal);
        }
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "test finish");
        jobject convertThenRetain = env->PopLocalFrame(lastVal);
        env->CallStaticVoidMethod(g_vm_class, g_dump_mid);
    }
    
    before:504
    after:504:addr:0x1d2007f9
    before:505
    // 临时帧与前一帧中505的local ref地址不同
    after:505:addr:0x1d2007fd
    test finish
    --- reference table dump ---
    JNI local reference table (0xb9858520) dump:
      Last 8 entries (of 8):
            // 临时帧与前一帧中505的local ref地址不同
            7: 0xa4fd0380 java.lang.String "505"
            6: 0xa4f759b8 cn.rexih.android.testnativeinterface.MainActivity
            5: 0xa4ce59f0 java.lang.Class<com.android.internal.os.ZygoteInit>
            4: 0xa4cffaf0 java.lang.String "start-system-ser... (19 chars)
            3: 0xa4cffa78 java.lang.String "com.android.inte... (34 chars)
            2: 0xa4cffa60 java.lang.String[] (2 elements)
            1: 0xa4c840e0 java.lang.Class<java.lang.String>
            0: 0xa4c831e8 java.lang.Class<java.lang.Class>
      Summary:
            3 of java.lang.Class (3 unique instances)
            3 of java.lang.String (3 unique instances)
            1 of java.lang.String[] (2 elements)
            1 of cn.rexih.android.testnativeinterface.MainActivity
    JNI global reference table (0xb9872390) dump:
      Last 10 entries (of 261):
          // ...
    

    Local Reference的代码实践

    1. local reference仅在本次jni方法调用过程中有效,jni方法执行完毕返回java层后,local reference失效,所以不能直接把local ref保存为全局变量。如果有需要可以使用Global/Global Weak ref保存为全局变量

    2. 不能产生大量的Local ref,尤其是在循环结构中,否则会造成table overflow ,每次循环结束,如果不再使用当前的local ref,应当及时删除

      env->DeleteLocalRef(pJstring);
      
    3. 按照使用场景,可以使用EnsureLocalCapacity或者Push/PopLocalFrame来控制Local Ref的规模或者管理销毁。

    Global Reference

    如果需要将某一个jobject及其子类转换成全局变量,必须使用Global Ref,但是必须始终跟踪全局引用,并确保不再需要对象时删除它们。

    JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
        JNIEnv *env = NULL;
        if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
            return -1;
        }
        g_vm_class = static_cast<jclass>(env->NewGlobalRef(env->FindClass("dalvik/system/VMDebug")));
        g_dump_mid = env->GetStaticMethodID(g_vm_class, "dumpReferenceTables", "()V");
        return JNI_VERSION_1_6;
    }
    
    JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) {
        JNIEnv *env = NULL;
        if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(env), JNI_VERSION_1_6)) {
            return;
        }
        env->DeleteGlobalRef(g_vm_class);
        g_dump_mid = NULL;
    }
    

    Global Weak Reference

    • 与全局引用类似,但是不阻止gc回收

    • 判断一个Global Weak Ref是否有效(所指向对象没有被回收),须要使用IsSameObject

      extern "C"
      JNIEXPORT jobject JNICALL
      Java_cn_rexih_android_testnativeinterface_MainActivity_testGetWeakGlobalRef(JNIEnv *env, jobject instance, jobject repl) {
          // 通过IsSameObject判断弱引用是否有效
          if (env->IsSameObject(g_weak, NULL)) {
              return repl;
          } else {
              return g_weak;
          }
      }
      
    • 使用案例

      JNIEXPORT void JNICALL Java_mypkg_MyCls_f(JNIEnv *env, jobject self) {
        static jclass myCls2 = NULL;
        if (myCls2 == NULL) {
            jclass myCls2Local = env->FindClass("mypkg/MyCls2");
            if (myCls2Local == NULL) {
                return; /* can’t find class */
            }
            myCls2 = env->NewWeakGlobalRef(myCls2Local);
            if (myCls2 == NULL) {
                return; /* out of memory */
            }
        }
        /* use myCls2 */
      }
      

    IsSameObject

    可以判断global/global weak/local ref指向的是不是同一个对象

    extern "C"
    JNIEXPORT jboolean JNICALL
    Java_cn_rexih_android_testnativeinterface_MainActivity_testIsSameObject(JNIEnv *env, jobject instance) {
        return env->IsSameObject(g_vm_class, env->FindClass("dalvik/system/VMDebug"));
    }
    

    临界区操作api的说明

    有一些问题,暂时不考虑使用,参见JVM Anatomy Park #9: JNI 临界区 与 GC 锁

    字符串操作

    string_function.png

    native string 转 java string

    jstring test = env->NewStringUTF("test");
    

    获取java字符串长度

    env->GetStringUTFLength(test);
    

    java string 转 native string

    choose_string_function.png
    • 临界区的操作有一些问题(见下文资料链接),尽量避免使用
    • 如果想自行管理字符串的内存,提高性能,考虑用GetStringUTFRegion,已获取字符串片段
    • Java String是不可变对象,所以转换成为native的const char*也不应该被修改,如果使用GetStringUTFChars,isCopy不应当被关注,传NULL即可

    字符串释放

    • ReleaseStringUTFChars: 使用GetStringUTFChars方式将java string 转 native string,使用完毕后须调用对应的Release方法释放字符串

      const char *byChars = env->GetStringUTFChars(testString, NULL);
      env->ReleaseStringUTFChars(testString, byChars);
      
    • DeleteLocalRef: 如果是在循环中NewStringUTF,本次循环结束时,如果不再使用(例如添加到string 数组后无其他操作),应当释放local ref。

    extern "C"
    JNIEXPORT jstring JNICALL
    Java_cn_rexih_android_testnativereference_JniManager_echo(JNIEnv *env, jobject instance, jstring text) {
        // java string -> const char*
        const char *byChars = env->GetStringUTFChars(text, NULL);
    
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "by GetStringUTFChars:%s", byChars);
        jstring pTemp = env->NewStringUTF(byChars);
        env->DeleteLocalRef(pTemp);
        
        env->ReleaseStringUTFChars(text, byChars);
    
        jsize len = env->GetStringUTFLength(text);
        char *regionBuf = new char[len];
        env->GetStringUTFRegion(text, 0, len, regionBuf);
    
        // const char* -> java string
        reverseChars(regionBuf);
        jstring pRet = env->NewStringUTF(regionBuf);
        delete regionBuf;
        return pRet;
    }
    

    编码转换

    1. 当前用ndk 18,cmake方式编译,中文字符串可以直接转换,不会出现乱码
    2. 如果需要转码,在native层通过FindClass使用String类的api来进行编码转换
    3. 如果确有需要使用这种方式,可以考虑把jclass和jmethodid缓存起来使用。

    数组操作

    array_function.png

    基本类型数组

    choose_array_function.png
    1. 如果要对一整块数据操作,可以考虑用Get<type>ArrayElements方法直接获取整块数据,比起反复获取少量数据效率更好,数据使用完后必须调用Release方法进行释放

    2. 如果有预分配的缓冲区或者只对部分数据进行操作,考虑用Get<type>ArrayRegion,将数据复制到缓冲区使用。不需要Release释放

      extern "C"
      JNIEXPORT jint JNICALL
      Java_cn_rexih_android_testnativereference_JniManager_testRegionArray(JNIEnv *env, jobject instance, jcharArray carr) {
      
          jsize alen = env->GetArrayLength(carr);
          jchar *pChar = new jchar[alen];
          env->GetCharArrayRegion(carr, 0, alen, pChar);
      
          int sum = 0;
      
          for (int i = 0; i < alen; ++i) {
              sum += (int) (pChar[i] - '0');
          }
          printRefTable(env);
          return sum;
      
      }
      

    isCopy/JNI_COMMIT/JNI_ABORT(同步处理)

    • Get<type>ArrayElements方法第二个参数isCopy是返回值,表示返回的数组数据,是原数据的拷贝还是原始内存内容。

      • 不同虚拟机的实现不同,4.4 dalvik虚拟机返回的是原始内存内容,9.0 art虚拟机返回的是拷贝。
      • 会返回拷贝的可能原因之一是由于内部存在大型数组,其中的数据可能不是连续的。通常,当数组的存储量小于堆的 1/1000 时,会作为直接指针返回。参见IBM copy and pin;另一种考虑是固定的对象无法压缩,并且会使整理碎片变得复杂,因此复制将减轻 GC 的负载。参见IBM isCopy
    • Release<type>ArrayElements第三个参数表示提交的模式:

      参见IBM Using the mode flag;JNI tips

      0
        Actual: the array object is un-pinned.
        Copy: data is copied back. The buffer with the copy is freed.
      JNI_COMMIT
        Actual: does nothing.
        Copy: data is copied back. The buffer with the copy is not freed.
      JNI_ABORT
        Actual: the array object is un-pinned. Earlier writes are not aborted.
        Copy: the buffer with the copy is freed; any changes to it are lost.
      
      • 如果是pinned的数组,只要修改了数组元素,因为是原始内存内容,所以何种提交模式后原始内存数据都被修改了;
      • 如果是copy数组,JNI_ABORT不会把copy里的修改提交到原始内存数据;
      • JNI_COMMIT,如果是pinned数组,观察ref table,发现被pinned的内存,在release后不会un-pinned,必须使用其他模式再次调用Release<type>ArrayElements
    extern "C" JNIEXPORT jint JNICALL
    Java_cn_rexih_android_testnativereference_JniManager_testArrayReleaseMode(
            JNIEnv *env, jobject instance, jintArray test, jint option) {
    
        jboolean isCopy;
        jint *pInt = env->GetIntArrayElements(test, &isCopy);
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "GetIntArrayElements isCopy: %s", isCopy ? "true" : "false");
        printRefTable(env);
        int a;
        switch (option) {
            case 0:
                // 0
                pInt[2] = pInt[2] + 12;
                env->ReleaseIntArrayElements(test, pInt, 0);
                printRefTable(env);
                a = pInt[1];
                break;
            case JNI_COMMIT:
                // commit
                pInt[2] = pInt[2] + 12;
                env->ReleaseIntArrayElements(test, pInt, JNI_COMMIT);
                printRefTable(env);
                a = pInt[1];
    //            env->ReleaseIntArrayElements(test, pInt, JNI_ABORT);
    //            printRefTable(env);
                break;
            case JNI_ABORT:
                // abort
                pInt[2] = pInt[2] + 12;
                env->ReleaseIntArrayElements(test, pInt, JNI_ABORT);
                printRefTable(env);
                a = pInt[1];
    
                break;
        }
        return a;
    }
    

    在9.0设备上,因为使用的是copy数组,所以local ref不需要特地说明

    在4.4设备上,使用的是copy数组,观察ref table

    // pinned数组,在ref table中有特殊记录:
    JNI pinned array reference table (0xb96f4060) dump:
      Last 1 entries (of 1):
            0: 0xa4fd1008 int[] (4 elements)
      Summary:
            1 of int[] (4 elements)
            
    // abort模式释放后 pinned数组 un-pinned
    JNI pinned array reference table (0xb96f4060) dump:
      (empty)
    // commit模式释放后 pinned数组 不会 un-pinned
    JNI pinned array reference table (0xb96f4060) dump:
      Last 1 entries (of 1):
            0: 0xa4fd1008 int[] (4 elements)
      Summary:
            1 of int[] (4 elements)
    

    二维数组和对象数组

    二位数组的每一维数组也是对象。

    • 对象数组只能通过数组下标获取到一个元素GetObjectArrayElement
    • 没有对应的Release方法,每次循环过程中,元素使用完毕后应当使用DeleteLocalRef删除多余的Local ref避免溢出
    extern "C"
    JNIEXPORT void JNICALL
    Java_cn_rexih_android_testnativereference_JniManager_testObjectArray(JNIEnv *env, jobject instance, jobjectArray objArray) {
    
        jstring pCurJstring;
        const char *pTmpChar;
        jsize alen = env->GetArrayLength(objArray);
        char **pCharArray = static_cast<char **>(malloc(sizeof(char *) * alen));
        for (int i = 0; i < alen; ++i) {
            pCurJstring = static_cast<jstring>(env->GetObjectArrayElement(objArray, i));
            pTmpChar = env->GetStringUTFChars(pCurJstring, NULL);
    
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "before %d: %s", i, pTmpChar);
    
            size_t clen = strlen(pTmpChar);
            char *pCpChar = static_cast<char *>(malloc(sizeof(char) * clen));
            strcpy(pCpChar, pTmpChar);
            reverseChars(pCpChar);
    
            pCharArray[alen - 1 - i] = pCpChar;
    
            env->ReleaseStringUTFChars(pCurJstring, pTmpChar);
            env->DeleteLocalRef(pCurJstring);
        }
    
        for (int i = 0; i < alen; ++i) {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "after %d: %s", i, pCharArray[i]);
            free(pCharArray[i]);
        }
        free(pCharArray);
        printRefTable(env);
        return;
    }
    

    java集合操作

    集合对象转换到jni方法的入参是jobject,使用集合可以通过两种方式:

    1. 通过classId和methodid来调用java层的api以获取和操作元素对象。转载一个demo

      // ...
      jclass cls_list = env->GetObjectClass(objectList);
      // ...
      jmethodID list_get = env->GetMethodID(cls_list, "get", "(I)Ljava/lang/Object;");
      jmethodID list_size = env->GetMethodID(cls_list, "size", "()I");
      // ...
      int len = static_cast<int>(env->CallIntMethod(objectList, list_size));
      // ...
      for (int i=0; i < len; i++) {
          jfloatArray element = (jfloatArray)(env->CallObjectMethod(objectList, list_get, i));
          
          float* f_arrays = env->GetFloatArrayElements(element,NULL);
          int arr_len = static_cast<int>(env->GetArrayLength(element));
          for(int j = 0; j < arr_len ; j++){
              printf("\%f \n", f_arrays[j]);
          }
          env->ReleaseFloatArrayElements(element, f_arrays, JNI_ABORT);
          
          env->DeleteLocalRef(element);
      }
      
    2. 对集合对象调用toArray(T[])方法转换成对象数组后再传入jni方法

    Exception处理

    • Java JNI 在检测到故障时不会抛出异常。本机代码负责检查是否发生异常。
    • 发生错误的情况下(返回值小于0/NULL),必须检查异常。
    • 在检查异常时,记住如果调用了 ExceptionDescribe() ,那么可能得到的描述过的异常是一段时间以前发生的,而不是最后一次调用的结果。
    • 当异常发生时,不要忘记调用ReleaseXXX释放资源。
    • native层需要通过Throw/ThrowNew抛出Java的异常类,ThrowNew可以设置异常的msg说明
    • native发生异常后,代码仍然执行,而不是立刻崩溃
    • native发生异常后,只可以调用部分jni的api方法,见下文

    第一种有缺陷的方式:

    if (0 > env->EnsureLocalCapacity(capacity)) {
        // 内存分配失败, 调用ExceptionOccurred也会返回一个jobject占用local ref table,须释放
        jthrowable pJthrowable = env->ExceptionOccurred();
        env->ExceptionDescribe();
        env->ExceptionClear();
        env->DeleteLocalRef(pJthrowable);
    }
    

    JNI ExceptionCheck 函数是比 ExceptionOccurred 调用更好的异常检查方式,因为 ExceptionOccurred 调用必须创建局部引用。

    参见IBM 处理异常

    if (env->ExceptionCheck()) {
        env->ExceptionDescribe();
        env->ExceptionClear();
    }
    

    异常发生时可以调用的jni api

    当异常待处理时,不能调用大多数JNI函数。您的代码应该会注意到异常(通过函数的返回值,ExceptionCheck或ExceptionOccurred)并返回,或者清除异常并处理它。
    当异常挂起时,您允许调用的JNI函数有:

    • DeleteGlobalRef
    • DeleteLocalRef
    • DeleteWeakGlobalRef
    • ExceptionCheck
    • ExceptionClear
    • ExceptionDescribe
    • ExceptionOccurred
    • MonitorExit
    • PopLocalFrame
    • PushLocalFrame
    • Release<PrimitiveType>ArrayElements
    • ReleasePrimitiveArrayCritical
    • ReleaseStringChars
    • ReleaseStringCritical
    • ReleaseStringUTFChars

    c++异常支持 TODO

    信号量捕获异常TODO

    参见JNI Crash:异常定位与捕获处理

    native操作java类与对象

    • 调用FindClass,GetMethodID,GetFieldID,默认会抛出java异常导致崩溃,根据需要可以考虑在调用后检查异常ExceptionCheckExceptionClear
    • class,methodID,fieldID的获取消耗性能,频繁调用可以考虑转化为全局引用并缓存到全局变量
    • NewObject,Call系列Method调用,Set系列Field设置方法,在调用前,应当检查入参是否缺失或者类型不匹配,否则会有不可预知的错误

    处理Class

    • FindClass,GetObjectClass,GetSuperclass可以分别从类的全路径,类的实例对象,父类获取到类的jclass对象;Object调用GetSuperclass会获取到NULL

    • IsInstanceOf与java中一样,可以判断一个对象是否是某个类或者其父类

    • IsAssignableFrom判断的是一个类是否是另一个的子类或者接口实现类

      Product [IsAssignableFrom] DetailProduct: false
      DetailProduct [IsAssignableFrom] Product: true
      

    创建Java对象

    • AllocObject可以无视构造函数和java类的初始化流程,仅仅是创建对象实例,分配内存,而不进行初始化。所有基本类型都是默认值,引用类型都是null

    • NewObject需要先使用GetMethodID找到所需要的构造函数,才可以创建实例对象,构造函数的方法名是<init>

      jmethodID pID = env->GetMethodID(pDetailProductClassByFind, "<init>", "()V");
      jobject object = env->NewObject(pDetailProductClassByFind, pID, /* args */);
      
    • NewObject参数说明:

      • NewObject只要构造函数的签名正确,即可创建对象实例,分配内存;

      • 即使传入的入参类型不匹配或者缺失也不会在native层产生异常,可以正常返回java层;

      • 但是java层在使用此对象时会产生不可预料的问题

      • 例如,应该传string,但是没有传或者传递错误的类型,虽然能够正常返回,但是当java层需要使用String时,会报stale reference

        E/dalvikvm: JNI ERROR (app bug): accessed stale weak global reference 0x7b (index 30 in a table of size 0)
        
        A/libc: Fatal signal 11 (SIGSEGV) at 0xdead4335 (code=1), thread 23145 (nativeinterface)
        

    调用Java方法

    • 由于成员方法有多态(虚函数),使用时须要注意:

      • 子类和父类的成员方法签名相同,都可以作为Call<Type>Method系列方法的入参,实际执行的方法看实例对象的真实类型,调用子类的方法。

      • 如果需要调用父类的方法(类似java的super),需要使用CallNonvirtual<Type>Method系列方法,与Call<Type>Method系列方法一样,子类或者父类的methodID都可以使用

        jstring pChildDesc = static_cast<jstring>(env->CallObjectMethod(entity, pChildDescMID));
        const char *cChildDesc = env->GetStringUTFChars(pChildDesc, NULL);
        jstring pTestParentDesc = static_cast<jstring>(env->CallObjectMethod(entity, pParentDescMID));
        const char *cTestParentDesc = env->GetStringUTFChars(pTestParentDesc, NULL);
        // 方法签名一样,不区别子类父类,都可以作为方法的methodID入参
        
    • CallStatic<Type>Method系列方法入参传入的是jclass而不是jobject

    操作Java成员变量

    主要就是获取fieldID后get/set

    GetFieldID
    GetStaticFieldID
    
    Get<Type>Field
    Set<Type>Field
    
    GetStatic<Type>Field
    SetStatic<Type>Field
    

    反射相关

    可以传入method/field对象,获取到methodID/fieldID,也可以反过来

    jfieldID    (*FromReflectedField)(JNIEnv*, jobject);
    jmethodID   (*FromReflectedMethod)(JNIEnv*, jobject);
    
    jobject     (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean);
    jobject     (*ToReflectedField)(JNIEnv*, jclass, jfieldID, jboolean);
    

    NIO相关TODO

    等了解java层的nio后再来看

    jobject     (*NewDirectByteBuffer)(JNIEnv*, void*, jlong);
    void*       (*GetDirectBufferAddress)(JNIEnv*, jobject);
    jlong       (*GetDirectBufferCapacity)(JNIEnv*, jobject);
    

    参考资料

    JNI.h解析

    JNI的数据类型和类型签名

    优化总结

    正确性缺陷

    • 使用错误的 JNIEnv

    • 未检测异常

    • 未检测返回值

    • 未正确使用数组方法

    • 未正确使用全局引用

    正确性技巧

    1. 仅在相关的单一线程中使用 JNIEnv
    2. 在发起可能会导致异常的 JNI 调用后始终检测异常。
    3. 始终检测 JNI 方法的返回值,并包括用于处理错误的代码路径。
    4. 不要忘记为每个 Get*XXX*() 使用模式 0(复制回去并释放内存)调用 Release*XXX*()
    5. 确保代码不会在 Get*XXX*Critical()Release*XXX*Critical() 调用之间发起任何 JNI 调用或由于任何原因出现阻塞。
    6. 不得将局部引用保存在全局变量中
    7. 始终跟踪全局引用,并确保不再需要对象时删除它们。

    性能缺陷

    • 不缓存方法 ID、字段 ID 和类
    • 触发数组副本
    • 回访(Reaching back)而不是传递参数
    • 错误认定本机代码与 Java 代码之间的界限
    • 使用大量本地引用,而未通知 JVM

    性能技巧

    1. 查找并全局缓存常用的类、字段 ID 和方法 ID。
    2. 获取和更新仅本机代码需要的数组部分。在只要数组的一部分时通过适当的 API 调用来避免复制整个数组。
    3. 在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。
    4. 如果可能,将各参数传递给 JNI 本机代码,以便本机代码回调 JVM 获取所需的数据。
    5. 定义 Java 代码与本机代码之间的界限,最大限度地减少两者之间的互相调用。
    6. 构造应用程序的数据,使它位于界限的正确的侧,并且可以由使用它的代码访问,而不需要大量跨界调用。
    7. 当本机代码造成创建大量本地引用时,在各引用不再需要时删除它们。
    8. 如果某本机代码将同时存在大量本地引用,则调用 JNI EnsureLocalCapacity()方法通知 JVM 并允许它优化对本地引用的处理。

    缓存ID的陷阱

    • 缓存FieldID需要注意子类和父类同名变量的问题;缓存MethodID不需要,因为会绑定到实例上
    • 考虑缓存FieldID的触发方法,放在父类的静态初始化代码块中调用,保证父类加载的时候先执行缓存方法,将正确的变量的FieldID缓存到本地代码中
    D类定义
    // Trouble in the absence of ID caching
    class D extends C {
        private int i;
        D() {
            f(); // inherited from C
        }
    }
    
    C类定义
    class C {
        private int i;
        native void f();
        private static native void initIDs();
        static {
            initIDs(); // Call an initializing native method
        }
    }
    
    本地JNI代码
       static jfieldID FID_C_i;
    
       JNIEXPORT void JNICALL
       Java_C_initIDs(JNIEnv *env, jclass cls) {
    
           /* Get IDs to all fields/methods of C that
              native methods will need. */
    
           FID_C_i = (*env)->GetFieldID(env, cls, "i", "I");
       }
    
       JNIEXPORT void JNICALL
       Java_C_f(JNIEnv *env, jobject this) {
    
           ival = (*env)->GetIntField(env, this, FID_C_i);
    
           ... /* ival is always C.i, not D.i */
       }
    
    

    其他

    • 记住在任何线程终止前调用 threadDetach() 。如果执行调用失败,在垃圾收集器运行时,可能导致大问题。它将试图查找已经不存在的线程的堆栈帧。

    参考资料

    IBM JNI核对表

    misc

    jni c与c++区别

    #if defined(__cplusplus)
    //C++ JNIEnv定义为_JNIEnv结构体
    typedef _JNIEnv JNIEnv;
    typedef _JavaVM JavaVM;
    #else
    //c JNIEnv定义为JNINativeInterface结构体指针
    typedef const struct JNINativeInterface* JNIEnv;
    typedef const struct JNIInvokeInterface* JavaVM;
    #endif
    
    • 定义的native接口方法入参的(JNIEnv *env, jobject instance),env在c++中是一个一级指针,而c中是一个二级指针,如果要在c使用env,必须先(*env)从二级指针取出一级指针地址。
    • c的结构体中没有this指针,所以c调用的jni方法的第一个参数需要传入env
    struct _JNIEnv {
        const struct JNINativeInterface* functions;
    #if defined(__cplusplus)
        // c++版本中,_JNIENV持有一个JNINativeInterface*成员变量,所有jni方法省略了第一个env参数,改为使用this指针
        jint GetVersion()
        { return functions->GetVersion(this); }
    #endif /*__cplusplus*/
    };
    

    参见androidNDK开发中c与C++的细小区别

    同步代码块TODO

    jint        (*MonitorEnter)(JNIEnv*, jobject);
    jint        (*MonitorExit)(JNIEnv*, jobject);
    

    在程序中集成JVM需要注意的JNI特征

    c预编译指令,可变参TODO

    va_list 、va_start、 va_arg、 va_end 使用说明

    预编译处理——#和##操作符的使用分析

    #define宏定义可变参数的使用

    C语言--预编译

    jboolean的陷阱

    • jboolean 是大小为1字节,值在0-255之间

    • 0表示JNI_FALSE,其他1-255表示JNI_TRUE

    • 如果数值超过256,则其低八位全是零,在被当做boolean时,高精度的类型降级会被截断,保留低八位,被认为是0表示false

    • 错误示例:

      int n = 256;
      print (n ? JNI_TRUE : JNI_FALSE);
      

    java层持久化c++对象

    将c++对象指针地址以long返回到java层保存。

    参见java 层调用Jni(Ndk) 持久化c c++ 对象

    参考资料

    JNI tips原版,JNI tips翻译

    JNI官方规范中文版

    在 JNI 编程中避免内存泄漏 对理解jni引用类型有很大帮助

    使用 Java Native Interface 的最佳实践避免最常见的 10 大 JNI 编程错误的技巧和工具

    相关文章

      网友评论

          本文标题:JNI代码实践

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