JNI 技巧 頂 原

JNI 是指 Java 本地層接口(Java Native Interface)。它爲用 Java 語言編寫的受控代碼定義了一種與本地層代碼(用 C/C++ 編寫)交互的方式。它是廠商無關的,其支持從動態共享庫加載代碼,儘管有時笨重,但它仍是有效的。

如果你對它還不熟悉,可以閱讀 JNI規範(Java Native Interface Specification) 來獲得對它的更多瞭解,瞭解 JNI 如何工作以及它有哪些功能。規範中有些地方的說明,並不是特別的清晰簡潔明瞭,因而接下來的一些內容也許有點用。

JavaVM 和 JNIEnv

JNI 定義了兩個關鍵的數據結構,JavaVMJNIEnv。它們都是指向函數表的指針。(在 C++ 版本中,它們是類,其中包含一個指向函數表的指針,及每個 JNI 函數對應一個的成員函數,這些成員函數則簡單地調用函數表中的對應函數。) JavaVM 提供了“調用接口”函數,通過這些函數,可以創建和銷燬一個 JavaVM。看一下 JavaVM 結構的定義就一目瞭然了:

#if defined(__cplusplus)
typedef _JavaVM JavaVM;
#else
typedef const struct JNIInvokeInterface* JavaVM;
#endif

/*
 * JNI invocation interface.
 */
struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

/*
 * C++ version.
 */
struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

理論上,每個進程可以有多個 JavaVM 實例,但 Android 只允許有一個。

JNIEnv 則提供了大多數的 JNI 函數。你的本地層函數都接受 JNIEnv 作爲其第一個參數。

JNIEnv 用於線程局部存儲。因此,你不能在線程之間共享同一個 JNIEnv。如果一段代碼沒有其它方法獲取它的 JNIEnv,你應該共享 JavaVM,並使用 JavaVM 結構的 GetEnv 函數找到線程的 JNIEnv。(假設它有一個;參見下面的 AttachCurrentThread。)

JNIEnvJavaVM 的 C 聲明與它們的 C++ 聲明不同。依賴於是被 include 進 C 還是 C++ 源文件中,"jni.h" 頭文件提供了不同的類型定義。因此,在兩個語言都會包含的頭文件中,包含 JNIEnv 參數並不是個好主意。(換句話說:如果你的頭文件需要#ifdef __cplusplus,且該頭文件中有任何內容引用了 JNIEnv,那麼你可能需要做一些額外的工作。)

比如定義了一個函數,其接受一個 JNIEnv 指針作爲參數。這個函數在 C 源文件和在 C++ 源文件中實現時,這個參數的類型實際上是不一樣的。對這個函數的調用,也要區分是在 C 代碼中調用還是在 C++ 代碼中調用,並做不同的處理。

線程

所有線程都是 Linux 線程,由內核調度。它們通常由 Java 代碼啓動 (使用 Thread.start 方法 ),但它們也可以在其它地方創建,然後附到 JavaVM 上。比如,一個由 pthread_create 啓動的線程,可以通過 JNI,即 JavaVM 實例的 AttachCurrentThreadAttachCurrentThreadAsDaemon 函數附到 JavaVM。在一個線程被附到 JavaVM 之前,它沒有 JNIEnv,因而也 不能執行 JNI 調用

把一個本底層創建的線程附接到 JavaVM 會創建一個  java.lang.Thread 對象,並添加到“main” ThreadGroup,使它對於調試器可見。對一個已經附接到 JavaVM 的線程調用 AttachCurrentThread 是一個空操作。

通過把本地層創建的線程附接到 JavaVM 中之後,也就可以在該線程中方便地調用 JNI 函數,訪問 Java 對象和結構了。

Android 不會掛起正在執行本地層代碼的線程。如果正在進行垃圾回收,或者調試器發出了一個掛起請求,Android 將在線程下一次執行 JNI 調用時暫停它。

通過 JNI 函數附接的線程在它們退出前必須調用 DetachCurrentThread。如果直接這樣寫代碼不方便,則在 Android 2.0 (Eclair) 或更高版本上,你可以使用pthread_key_create 定義一個將會在線程退出前被調用的析構函數,並在那兒調用 DetachCurrentThread。(以該 key 調用 pthread_setspecific 來將 JNIEnv 保存在線程局部存儲中;以此,它將會作爲參數被傳進你的析構函數。)

jclass,jmethodID,和 jfieldID

如果你想在本地層代碼中訪問 Java 對象的成員,你將需要執行以下操作:

  • 通過 FindClass 獲取該類的類對象引用
  • 通過 GetFieldID 獲取成員的成員 ID 對象引用
  • 通過適當的方法獲取成員的內容,比如 GetIntField

類似地,要調用一個方法,你首先要獲取類對象的引用,然後獲得方法 ID 對象引用。IDs 經常只是指向內部運行時數據結構的指針。查找它們可能需要一些字符串比較,然而一旦有了它們,獲取成員或者調用方法的實際調用是非常快的。

如果性能很重要,在你的本底層代碼中,進行一次查找操作並將結果緩存起來會很有用。由於有着每個進程一個 JavaVM 實例的限制,把這些數據保存在一個靜態本地結構中是合理的。

在類被卸載之前,類引用,成員 IDs,和方法 IDs 會保證是有效的。只有當與一個 ClassLoader 相關聯的所有類都可以被垃圾回收時,類纔會被卸載,這很罕見,但在 Android 中也不是不可能。然而,注意 jclass 是一個類引用,且 必須通過調用 NewGlobalRef 來保護 (參見下一節)。

如果你想在類被加載時緩存 IDs,並在類被卸載且重新加載時自動地重新緩存它們,初始化 IDs 的正確方法是,在適當的類中添加一段像下面這樣的代碼:

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

在你的 C/C++ 代碼中創建一個 nativeClassInit 方法執行 ID 查找。代碼將會在類初始化時執行一次。如果類被卸載並重新加載,它將會再次執行。

局部和全局引用

傳遞給本地層方法的每個參數,和由 JNI 函數返回的幾乎每個對象均是一個 “局部引用”。這意味着它在當前線程的當前本地層方法運行期間是有效的。即使對象本身在本地層方法返回之後繼續存活,引用依然不是有效的

這適用於所有的 jobject 子類,包括 jclassjstring,和 jarray。(當啓用擴展 JNI 檢查時,運行時將爲大多數引用的錯誤使用發出警告。)

獲得非局部引用的僅有的方法是通過函數 NewGlobalRefNewWeakGlobalRef

如果你想更長期地持有引用,你必須使用一個“全局的”引用。NewGlobalRef 函數接收局部引用作爲參數,並返回一個全局引用。全局引用保證是有效的,直到你調用 DeleteGlobalRef

這一模式常被用於緩存 FindClass 返回的 jclass,如:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

如果這樣說的話,那 jmethodID 和 jfieldID 呢?

所有的 JNI 方法都接收局部和全局的引用作爲參數。可能引用同一個對象的引用具有不同的值。比如,連續地對相同對象調用 NewGlobalRef 獲得的返回值可能不同。要查看兩個引用是否指向相同對象,你必須使用 IsSameObject 函數。千萬不要在本地層代碼中用 == 比較引用。

這樣的結果是在本地層代碼中你 一定不能假設對象引用是常量或唯一的。在對一個方法的一次調用和下一次調用之間表示對象的 32 位值可能不同, 在連續調用中兩個不同的對象可能具有相同的 32 位值。不要使用 jobject 值作爲鍵。

程序員需要 “不過分地分配”局部引用。實際上,這意味着如果你正在創建大量的局部引用,也許在遍歷一個對象數組,你應該使用 DeleteLocalRef 手動釋放它們,而不是讓 JNI 爲你執行。實現只被要求爲局部引用保留 16 個槽,因此如果你需要更多,你應該或者在運行過程中刪除一些,或者使用 EnsureLocalCapacity / PushLocalFrame 保留更多。

注意 jfieldIDjmethodID 是不透明類型,而不是對象引用,且不應該被傳給 NewGlobalRef。像 GetStringUTFCharsGetByteArrayElements 這樣的函數返回的原始數據指針也不是對象。(它們可以在線程間傳遞,且直到對應的 Release 被調用都是有效的。)

一種不常見的情況應該另外提一下。如果你通過 AttachCurrentThread 附了一個本地層線程,你執行的代碼將從不會自動地釋放局部引用,直到線程分離。你創建的任何局部引用將不得不手動刪除。通常,在循環中創建局部引用的任何本地層代碼可能需要執行一些手動刪除。

UTF-8 和 UTF-16 字符串

Java 編程語言使用 UTF-16。爲了方便,JNI 也提供方法使用 改進的 UTF-8。改進的編碼對 C 代碼很有用,因爲它把 \u0000 編碼爲 0xc0 0x80 而不是 0x00。關於這一點的好處是,您可以依靠具有 C 風格的以零爲終止字符的字符串,適合與標準 libc 的字符串函數一起使用。

缺點是你不能傳遞任意的 UTF-8 數據給 JNI 並期待它能正確工作。

如果可能,操作 UTF-16 字符串通常更快。當前 Android 在 GetStringChars 中不需要拷貝,然而 GetStringUTFChars 需要分配並轉換爲 UTF-8。注意 **UTF-16 字符串不是以 0 結尾的,**且允許 \u0000 ,所以你需要根據字符串的長度來訪問 jchar 指針。

不要忘記 ReleaseGet 的字符串。字符串函數返回 jchar*jbyte*,它們都是指向原始數據類型數據的 C 風格指針,而不是局部引用。它們保證有效,直到調用 Release,這意味着當本地層方法返回時它們不會釋放。

傳遞給 NewStringUTF 的數據必須是改進的 UTF-8 格式。一個常見的錯誤是,從文件或網絡流讀取字符數據並在無過濾的情況下交給 NewStringUTF 處理。除非你知道數據是 7 位的 ASCII,你需要刪除高 ASCII 字符或將其轉換爲正確的改進 UTF-8 格式。如果你沒有,UTF-16 轉換可能不是你期待的那樣。擴展的 JNI 檢查將掃描字符串,並就無效數據向你提出警告,但它們不會捕獲所有東西。

原始數據類型的數組

JNI 提供了訪問數組對象內容的函數。儘管每次只能訪問一個數組對象的項,但原始數據類型的數組可以直接讀或寫,就像它們在 C 中聲明的一樣。

爲了使接口儘可能高效且,Get<PrimitiveType>ArrayElements 族調用允許運行時返回指向實際元素的指針,或分配一些內存並拷貝一份。無論哪種方式,返回的原始指針保證有效,直到對應的 Release 被調用(這表明,如果數據沒有拷貝,則數組對象將被固定,並且不能作爲壓縮堆的一部分重新定位)。你必須 ReleaseGet 的每個數組。此外,如果 Get 調用失敗,你必須確保你的代碼沒有在後面試圖 Release 一個 NULL 指針。

你可以通過傳遞一個非空指針作爲 isCopy 參數決定是否拷貝數據。這很少用到。

Release 調用接收一個 mode 參數,其可以是三個值中的一個。運行時執行的行爲依賴於它是返回一個指向實際數據的指針還是實際數據拷貝的指針:

  • 0
    • 實際數據:數組對象是未固定的。
    • 拷貝:數據被烤回。拷貝的緩衝區被釋放。
  • JNI_COMMIT
    • 實際數據:什麼也不做。
    • 拷貝:數據被烤回。拷貝的緩衝區 不釋放
  • 0
    • 實際數據:數組對象是未固定的。早期寫入 不會 中止。
    • 拷貝:拷貝的緩衝區被釋放;任何修改丟失。

檢查 isCopy 的一個原因是瞭解在對數組做了修改後你是否需要以 JNI_COMMIT 調用 Release—— 如果你在改變數組內容和執行使用數組內容的代碼之間進行交替,你可能可以跳過無操作提交。另一個檢查標記的可能原因是高效的處理 JNI_ABORT。比如,你也許想要獲得一個數組,修改它,傳遞一部分給其它函數,然後丟棄修改。如果你知道 JNI 爲你創建了一份拷貝,則無需創建另一份“可編輯的”拷貝。如果 JNI 向你傳遞了原始的,則你確實需要創建你自己的拷貝。

一個常見的錯誤是(在示例代碼中重現)假設你可以在 * isCopy 爲 false 時跳過調用 Release。這不是實際的情況。如果沒有分配拷貝緩衝區,則原始的內存必須被固定下來,且不能由垃圾收集器移動。

還要注意 JNI_COMMIT 標記 釋放數組,在最後你將需要以一個不同的標記再次調用 Release

區域調用

當你想做的就只是拷入拷出數據,有另外一些像 Get<Type>ArrayElementsGetStringChars 的調用可能非常有用。考慮下面的代碼:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

獲取數組,拷貝前面的 len 字節的元素,然後釋放數組。Get 調用是固定還是拷貝數組的內容依賴於實現。代碼拷貝數據(也許是第二次),然後調用 Release;在這種情況下 JNI_ABORT 確保沒有第三個副本的機會。

可以完成相同事情的更簡單的代碼如下:

    env->GetByteArrayRegion(array, 0, len, buffer);

這有幾個優勢:

  • 需要一個 JNI 調用而不是 2 個,減少了開銷。
  • 不需要固定或額外的數據拷貝。
  • 降低了程序員錯誤的風險 - 沒有了在一些失敗後忘記調用 Release 的風險。

類似地,你可以使用 Set<Type>ArrayRegion 調用把數據複製到一個數組,及 GetStringRegionGetStringUTFRegion 把字符拷出一個 String

異常

你一定不能在異常掛起時調用大多數 JNI 函數。你的代碼需要注意到異常(通過函數的返回值,ExceptionCheck,或 ExceptionOccurred)並返回,或清除異常並處理它。

在異常掛起時你能調用的 JNI 函數只有下面這些:

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

許多 JNI 調用可能拋出異常,但也常提供簡單的方式用於失敗檢查。比如,如果 NewString 返回非空值,你不需要檢查失敗。然而,如果你調用了一個方法(使用像 CallObjectMethod 這樣的函數),你必須總是檢查異常,因爲如果拋出了異常,返回值將不是有效的。

注意,由解釋器代碼拋出的異常無法展開本地層棧幀,且 Android 還不支持 C++ 異常。JNI ThrowThrowNew 指令只是在當前線程中設置一個異常指針。一旦從本地層代碼返回受控代碼,異常將被注意到並被適當地處理。

本地層代碼可以通過調用 ExceptionCheckExceptionOccurred “捕獲”異常,並通過 ExceptionClear 清除它。通常,丟棄異常而不處理它們可能產生一些問題。

沒有內置的函數管理 Throwable 對象本身,因此如果你想獲取異常字符串,你將需要找到 Throwable 類,查找 getMessage "()Ljava/lang/String;" 的方法 ID,調用它,如果結果非空,則使用 GetStringUTFChars 獲得一些你可以交給 printf(3) 或等價的函數的東西。

擴展檢查

JNI 執行非常少的錯誤檢查。錯誤通常導致崩潰。Android 還提供了一個稱爲 CheckJNI 的模式,其中 JavaVM 和 JNIEnv 函數表指針被切換爲,在調用標準實現前執行一系列擴展檢查的函數的表。

額外的檢查包括:

  • 數組:嘗試分配一個負值大小的數組。
  • 壞指針:傳遞一個壞的 jarray/jclass/jobject/jstring 給 JNI 調用,或傳遞一個空指針作爲不能爲空的參數給 JNI 調用。
  • 類名:使用任何 “java/lang/String”形式的類名調用 JNI。
  • 臨界調用:在一個 “critical”get 和它對應的 release 之間執行一個 JNI 調用。
  • Direct ByteBuffers:給 NewDirectByteBuffer 傳遞壞的參數。
  • 異常:在異常掛起的時候執行 JNI 調用。
  • JNIEnvs:在錯誤的線程中使用 JNIEnvs。
  • jfieldIDs:使用一個空的 jfieldIDs,或使用一個 jfieldID 給字段設置錯誤類型的值(比如,試圖將一個 String 字段賦值爲一個 StringBuilder),或使用靜態字段的 jfieldID 來設置一個實例字段,反之亦然,或將一個類的 jfieldID 用於另一個類的實例。
  • jmethodIDs:當執行 Call*Method JNI 調用時使用了錯誤種類的 jmethodID:不正確的返回類型,靜態/非靜態不匹配,錯誤類型的 ‘this’(對於非靜態調用)或錯誤的類(對於靜態調用)。
  • 引用:在錯誤的引用種類上使用 DeleteGlobalRef/DeleteLocalRef
  • 釋放模式:傳遞一個壞的釋放模式給釋放調用(0JNI_ABORT,或 JNI_COMMIT 之外的東西)。
  • 類型安全:在你的本地層方法中返回一個不兼容的類型(比如,在一個聲明爲返回 String 的方法中返回一個 StringBuilder)。
  • UTF-8:給 JNI 調用傳遞一個無效的 改進的 UTF-8 字節序列。

(方法和字段的可訪問性依然沒有檢查:訪問限制不適用於本地層代碼。)

有多種方法啓用 CheckJNI。

如果你正在使用模擬器,CheckJNI 是默認開啓的。

如果你有一個經過 root 的設備,你可以使用下面的命令啓用 CheckJNI 模式重啓運行時:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

在所有這些情況中,你將在 logcat 輸出中運行時啓動時看到像這樣的東西:

D AndroidRuntime: CheckJNI is ON

如果你有一個普通的設備,你可以使用下面的命令:

adb shell setprop debug.checkjni 1

這不影響已經運行的應用,但自那之後啓動的應用都將開啓 CheckJNI。(將屬性修改爲其它值或簡單地重啓將再次禁用 CheckJNI。)在這種情況下,你將在你的 logcat 輸出中下次啓動一個應用時看到像這樣的東西:

D Late-enabling CheckJNI

你還可以在你的應用的 manifest 中設置 android:debuggable 屬性來只爲你的應用開啓 CheckJNI。注意 Android 構建工具將自動地爲某一構建類型做這些。

本地庫

你可以通過標準的 System.loadLibrary 調用從共享庫加載本地層代碼。獲取你本地層代碼的首選方法是:

  • 在一個靜態的類初始化器裏調用 System.loadLibrary。(參考前面的例子,其中用於調用 nativeClassInit 的那個。)參數是“未修飾的”庫名,比如要加載 "libfubar.so",你應該傳遞 "fubar"。
  • 提供一個本地層函數: jint JNI_OnLoad(JavaVM* vm, void* reserved)
  • JNI_OnLoad,註冊你所有的本地層方法。你應該聲明那些方法爲 "static",使那些名稱不佔用設備的符號表空間。

如果用 C++ 寫的話,JNI_OnLoad 函數看起來應該像下面這樣:

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // Get jclass with env->FindClass.
    // Register methods with env->RegisterNatives.

    return JNI_VERSION_1_6;
}

你也可以用共享庫的完整路徑名調用 System.load。對於Android 應用,你也許會發現從 context 對象獲取應用程序私有數據存儲區的完整路徑的方法非常有用。

這是建議採用的方法,但不是唯一的方法。無需顯式的註冊,你也不是必須提供 JNI_OnLoad 函數。你可以使用以特殊的方式命名本地層方法的“發現機制”來代替 (詳情參看 JNI spec),儘管這種方法更不盡如人意。如果方法簽名錯了的話,在方法第一次被實際調用之前,你都將無法獲知這種情況。

關於 JNI_OnLoad 另一點需要注意的是:你所作出的任何對於 FindClass 的調用發生在用於加載共享庫的類加載器的上下文。通常,FindClass 使用與解釋棧頂端的方法相關聯的加載器,或者如果沒有(由於線程只是被附接的)它使用“system”類加載器。這使得 JNI_OnLoad 成爲查找和緩存類對象引用的適當場所。

64 位注意事項

Android 當前主要運行於 32 位平臺。理論上,可以爲 64 位系統構建它,但那不是目前的目標。對於大多數部分來說,這不是你在與本地層代碼交互時需要擔憂的,但如果你計劃將指向本地層結構的指針保存在對象的 integer 字段中,它就變得非常重要了。要支持使用 64 位指針的架構,你需要在 long 字段中保存你的本地層指針而不是 int

不支持的功能/向後兼容性

所有的 JNI 1.6 功能都支持,以下這些例外:

  • DefineClass 沒有實現。 Android 不使用 Java 字節碼或類文件,因此傳遞二進制類數據無法工作。

爲了與老版本 Android 保持向後兼容性,你可能需要意識到如下這些:

  • 本地層函數的動態查找 直到 Android 2.0 (Eclair), 在搜索方法名期間 '$' 字符都不被適當地轉爲 "_00024"。爲了繞過這個問題,需要顯式地註冊或將本地層方法移出內部類。
  • 分離線程 直到 Android 2.0 (Eclair), 都無法使用 pthread_key_create 析夠函數來避免“線程必須在退出前分離”檢查。(運行時也使用 pthread key 析夠函數,因此查看誰首先被調用將有一個競態。)
  • 弱全局引用 直到 Android 2.0 (Eclair), 弱全局引用都沒有實現。更老的版本將直接地拒絕使用它們的嘗試。你可以使用 Android 平臺版本常量測試對它的支持。 直到 Android 4.0 (Ice Cream Sandwich),弱全局引用只能傳遞給 NewLocalRefNewGlobalRefDeleteWeakGlobalRef。(規範強烈鼓勵程序員在通過它們做任何事之前,創建到弱全局引用的硬引用,因此這不應該是限制。) 對於 Android 4.0 (Ice Cream Sandwich),弱全局引用可以像任何其它 JNI 引用那樣使用。
  • 局部引用 直到 Android 4.0 (Ice Cream Sandwich),局部引用實際上是直接指針。Ice Cream Sandwich添加了間接指針以支持更好的垃圾回收,但這意味着很多 JNI 錯誤在舊版本上是不可檢測的。參考 ICS 中的 JNI 局部引用變化 瞭解更多詳情。
  • 通過 GetObjectRefType 確定引用類型 直到 Android 4.0 (Ice Cream Sandwich),作爲使用直接指針的結果(參考上文),正確地實現 GetObjectRefType 都是不可能的。相反我們使用一種啓發式的方法,按順序查找弱全局表,參數,局部表,和全局表。它第一次找到你的直接指針,它將報告你的引用具有它恰巧在檢測的類型。這意味着,比如,如果你在一個全局 jclass 上調用 GetObjectRefType,碰巧與作爲隱式參數傳遞給你的靜態本地方法的 jclass 相同,你將獲得 JNILocalRefType 而不是 JNIGlobalRefType

FAQ:爲什麼我遇到了 UnsatisfiedLinkError

當你使用本地層代碼時,像下面這樣的失敗比較常見:

java.lang.UnsatisfiedLinkError: Library foo not found

在某些情況下它的含義就像它說的那樣 - 庫找不到。在其它情況中,庫存在但是無法被 dlopen(3)打開,失敗的詳情可以在異常的細節消息中找到。

你可能遇到 "library not found" 異常的常見原因如下:

  • 庫不存在或應用無法訪問。使用 adb shell ls -l <path> 檢查它是否存在,以及權限。
  • 庫不是用 NDK 構建的。這可能導致依賴的函數或庫在設備上不存在。

另一種類的 UnsatisfiedLinkError 失敗看上去像這樣:

java.lang.UnsatisfiedLinkError: myfunc
       at Foo.myfunc(Native Method)
       at Foo.main(Foo.java:10)

在 logcat 中,你將看到:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

這意味着運行時試圖找到一個匹配的方法,但未成功。這種情況一些常見的原因如下:

  • 庫沒有加載。檢查 logcat 關於庫加載的輸出消息。
  • 由於名字或簽名不匹配,方法沒有找到。這通常由於一下原因引起:
    • 對於延遲方法查找,以 extern "C" 和適當的可見性 (JNIEXPORT) 聲明 C++ 函數失敗。注意,在 Ice Cream Sandwich 之前,JNIEXPORT 宏是不正確的,因此以老的 jni.h 使用新的 GCC 無法工作。你可以使用 arm-eabi-nm 查看庫中出現的符號;如果它們看起來是修飾過的(比如 _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass 而不是 Java_Foo_myfunc),或者如果符號類型是小寫的 't' 而不是大寫的 'T',則你需要調整聲明。
    • 對於顯式的註冊,輸入方法簽名時的小錯誤。確保傳遞給註冊調用的東西與日誌文件中的簽名匹配。記得 'B' 是 byte 且 'Z' 是 boolean。簽名中的類名組件以 'L' 開頭,以 ';' 結尾,使用 '/' 分割包/類名,並使用 '$' 分割內部類名字(比如,Ljava/util/Map$Entry;)。

使用 javah 自動地生成 JNI 頭部也許對避免一些錯誤有幫助。

爲什麼 FindClass 找不到我的類?

(這個建議的大部分等價地適用於通過 GetMethodIDGetStaticMethodID 查找方法,或通過 GetFieldIDGetStaticFieldID 查找字段的失敗。)

確保類名字符串具有正確的格式。JNI 類名以包名開始,且有斜線分割,比如 java/lang/String。如果你在查找一個數組類,你需要以適當數量的方括號開始,且還必須以 'L' 和 ';' 包裹類,因此一維的 String 數組將是 [Ljava/lang/String;。如果你在查找一個內部類,使用 '$' 而不是 ','。通常,在 .class 文件上使用 javap 是找到你的類的內部名字的一種好方法。

如果你在使用 ProGuard,請確保 ProGuard 沒有剝去你的類。這可能在你的類/方法/字段只有 JNI 使用時發生。

如果類名稱正確,則可能遇到了類加載器問題。FindClass 想要在與你的代碼關聯的類加載器中開始類搜索。如果檢查調用棧,它將看起來像這樣:

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

最上面的方法是 Foo.myfuncFindClass 查找與 Foo 類關聯的 ClassLoader 對象並使用它。

這通常執行了你想要的。如果您自己創建一個線程(可能通過調用pthread_create,然後使用 AttachCurrentThread 連接),您可能會遇到麻煩。現在沒有你的應用的棧幀。如果你在這個線程中調用 FindClass,JavaVM 將在 "system" 類加載器中啓動而不是與你的應用關聯的那個,因此嘗試查找應用特有的類將失敗。

有一些方法繞過這個問題:

  • JNI_OnLoad 中執行你的 FindClass 一次,並緩存類引用以備後用。任何作爲執行 JNI_OnLoad 的一部分對 FindClass 所做的調用將使用與調用 System.loadLibrary 的函數關聯的類加載器(這是一個特殊的規則,用來使庫初始化更方便)。如果你的應用代碼正在加載庫,FindClass 將使用正確的類加載器。
  • 給需要的函數傳遞一個類的實例,通過聲明你的本地層方法接收一個 Class 參數,並傳入 Foo.class
  • 在方便的地方緩存 ClassLoader 對象的引用,然後直接觸發 loadClass 調用。這需要一些力氣。

FAQ:我如何與本地層代碼共享原始數據

你可能發現你自己需要同時在 Java 代碼和本地層代碼中訪問一個巨大的原始數據緩衝區。常見的例子包括管理 bitmaps 和聲音採樣。有兩個基本的方法。

你可以把數據存儲在 byte[] 中。這允許在 Java 代碼中非常快速的訪問。在本地層代碼中,然而,無法保證在不復制數據的情況下能夠訪問數據。在一些實現中, GetByteArrayElementsGetPrimitiveArrayCritical 將返回指向 Java 堆中的原始數據的實際指針,但在其它實現中,它將在本地層堆上分配一塊緩衝區並複製數據。

另一種方法是把數據存儲進直接字節緩衝區。這些可以用 java.nio.ByteBuffer.allocateDirect ,或JNI NewDirectByteBuffer 函數創建。不像普通的字節緩衝區,不在 Java 堆上分配存儲,且總是可以在本地層代碼中直接訪問(通過 GetDirectBufferAddress 獲得地址)。依賴於直接字節緩衝區訪問如何實現,在 Java 代碼中訪問可能非常慢。

選擇使用哪種依賴於兩個因素:

  • 大多數的數據訪問是發生在 Java 代碼中還是 C/C++ 代碼中?
  • 如果數據被最終傳遞給一個系統 API,它的形式必須是什麼?(比如,如果數據最終被傳遞給接收 byte[] 的函數,在一個直接 ByteBuffer 中執行處理可能是不明智的。)

如果沒有清晰的贏家,使用直接字節緩衝區。對它們的支持是直接內建在 JNI 中的,且在未來的版本中性能應該有提升。

原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章