JNI程式碼實踐

weixin_34249678發表於2018-12-17

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及其子類物件,就會往該表中新增一條引用記錄。

2950351-ff16069043bcc213.jpg
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 鎖

字串操作

2950351-4763f28bbf671258.png
string_function.png

native string 轉 java string

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

獲取java字串長度

env->GetStringUTFLength(test);

java string 轉 native string

2950351-ef60ee5e3aa338c3.png
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快取起來使用。

陣列操作

2950351-b7b5910a35e3eb3b.png
array_function.png

基本型別陣列

2950351-0751ff3c89b4b10d.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 程式設計錯誤的技巧和工具

相關文章