JNI 技巧
JNI 是指 Java 本地層接口(Java Native Interface)。它為用 Java 語言編寫的受控代碼定義了一種與本地層代碼(用 C/C++ 編寫)交互的方式。它是廠商無關的,其支持從動態共享庫加載代碼,盡管有時笨重,但它仍是有效的。
如果你對它還不熟悉,可以閱讀 JNI規范(Java Native Interface Specification)?來獲得對它的更多了解,了解 JNI 如何工作以及它有哪些功能。規范中有些地方的說明,并不是特別的清晰簡潔明了,因而接下來的一些內容也許有點用。
JavaVM 和 JNIEnv
JNI 定義了兩個關鍵的數據結構,JavaVM 和 JNIEnv。它們都是指向函數表的指針。(在 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。)
JNIEnv 和 JavaVM 的 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 實例的 AttachCurrentThread 或 AttachCurrentThreadAsDaemon 函數附到 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 子類,包括 jclass,jstring,和 jarray。(當啟用擴展 JNI 檢查時,運行時將為大多數引用的錯誤使用發出警告。)
獲得非局部引用的僅有的方法是通過函數 NewGlobalRef 和 NewWeakGlobalRef。
如果你想更長期地持有引用,你必須使用一個“全局的”引用。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 保留更多。
注意 jfieldID 和 jmethodID 是不透明類型,而不是對象引用,且不應該被傳給 NewGlobalRef。像 GetStringUTFChars 和 GetByteArrayElements 這樣的函數返回的原始數據指針也不是對象。(它們可以在線程間傳遞,且直到對應的 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 指針。
不要忘記 Release 你 Get 的字符串。字符串函數返回 jchar* 或 jbyte*,它們都是指向原始數據類型數據的 C 風格指針,而不是局部引用。它們保證有效,直到調用 Release,這意味著當本地層方法返回時它們不會釋放。
傳遞給 NewStringUTF 的數據必須是改進的 UTF-8 格式。一個常見的錯誤是,從文件或網絡流讀取字符數據并在無過濾的情況下交給 NewStringUTF 處理。除非你知道數據是 7 位的 ASCII,你需要刪除高 ASCII 字符或將其轉換為正確的改進 UTF-8 格式。如果你沒有,UTF-16 轉換可能不是你期待的那樣。擴展的 JNI 檢查將掃描字符串,并就無效數據向你提出警告,但它們不會捕獲所有東西。
原始數據類型的數組
JNI 提供了訪問數組對象內容的函數。盡管每次只能訪問一個數組對象的項,但原始數據類型的數組可以直接讀或寫,就像它們在 C 中聲明的一樣。
為了使接口盡可能高效且,Get<PrimitiveType>ArrayElements 族調用允許運行時返回指向實際元素的指針,或分配一些內存并拷貝一份。無論哪種方式,返回的原始指針保證有效,直到對應的 Release 被調用(這表明,如果數據沒有拷貝,則數組對象將被固定,并且不能作為壓縮堆的一部分重新定位)。你必須 Release 你 Get 的每個數組。此外,如果 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>ArrayElements 和 GetStringChars 的調用可能非常有用。考慮下面的代碼:
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 調用把數據復制到一個數組,及 GetStringRegion 或 GetStringUTFRegion 把字符拷出一個 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 Throw 和 ThrowNew 指令只是在當前線程中設置一個異常指針。一旦從本地層代碼返回受控代碼,異常將被注意到并被適當地處理。
本地層代碼可以通過調用 ExceptionCheck 或 ExceptionOccurred “捕獲”異常,并通過 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 調用。
- JNIEnv*s:在錯誤的線程中使用 JNIEnv*s。
- jfieldIDs:使用一個空的 jfieldIDs,或使用一個 jfieldID 給字段設置錯誤類型的值(比如,試圖將一個 String 字段賦值為一個 StringBuilder),或使用靜態字段的 jfieldID 來設置一個實例字段,反之亦然,或將一個類的 jfieldID 用于另一個類的實例。
- jmethodIDs:當執行 Call*Method JNI 調用時使用了錯誤種類的 jmethodID:不正確的返回類型,靜態/非靜態不匹配,錯誤類型的 ‘this’(對于非靜態調用)或錯誤的類(對于靜態調用)。
- 引用:在錯誤的引用種類上使用 DeleteGlobalRef/DeleteLocalRef。
- 釋放模式:傳遞一個壞的釋放模式給釋放調用(0,JNI_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),弱全局引用只能傳遞給 NewLocalRef,NewGlobalRef 和 DeleteWeakGlobalRef。(規范強烈鼓勵程序員在通過它們做任何事之前,創建到弱全局引用的硬引用,因此這不應該是限制。)
對于 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: myfuncat 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 找不到我的類?
(這個建議的大部分等價地適用于通過 GetMethodID 或 GetStaticMethodID 查找方法,或通過 GetFieldID 或 GetStaticFieldID 查找字段的失敗。)
確保類名字符串具有正確的格式。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.myfunc。FindClass 查找與 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 代碼中非常快速的訪問。在本地層代碼中,然而,無法保證在不復制數據的情況下能夠訪問數據。在一些實現中, GetByteArrayElements 和 GetPrimitiveArrayCritical 將返回指向 Java 堆中的原始數據的實際指針,但在其它實現中,它將在本地層堆上分配一塊緩沖區并復制數據。
另一種方法是把數據存儲進直接字節緩沖區。這些可以用 java.nio.ByteBuffer.allocateDirect ,或JNI NewDirectByteBuffer 函數創建。不像普通的字節緩沖區,不在 Java 堆上分配存儲,且總是可以在本地層代碼中直接訪問(通過 GetDirectBufferAddress 獲得地址)。依賴于直接字節緩沖區訪問如何實現,在 Java 代碼中訪問可能非常慢。
選擇使用哪種依賴于兩個因素:
- 大多數的數據訪問是發生在 Java 代碼中還是 C/C++ 代碼中?
- 如果數據被最終傳遞給一個系統 API,它的形式必須是什么?(比如,如果數據最終被傳遞給接收 byte[] 的函數,在一個直接 ByteBuffer 中執行處理可能是不明智的。)
如果沒有清晰的贏家,使用直接字節緩沖區。對它們的支持是直接內建在 JNI 中的,且在未來的版本中性能應該有提升。
原文
總結
- 上一篇: javaScript 内存管理机制
- 下一篇: EventBus设计与实现分析——特性介