Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android官方開發文檔Training系列課程中文版:Android的JNI相關

Android官方開發文檔Training系列課程中文版:Android的JNI相關

編輯:關於Android編程

JNI的全稱為Java Native Interface,中文意思是Java本地接口。它定義了Java代碼與C/C++代碼之間的交互方式。它是兩者的橋梁,支持從動態共享庫中加載代碼。雖然有些復雜,但是它的執行效率還是蠻高的。

如果你對JNI還不太熟悉,那麼可以通過Java Native Interface Specification來了解一下JNI的大致工作流程以及JNI的特性。

JavaVM與JNIEnv

JNI定義了兩個關鍵的數據結構:”JavaVM”與”JNIEnv”。這兩個函數本質上都為指向函數指針的指針表。JavaVM提供了”接口調用”功能,該功能允許創建、銷毀JavaVM。理論上每個進程可以擁有多個虛擬機,但是在Android中只允許出現一個。

JNIEnv提供了大部分的JNI功能。任何本地方法都以JNIEnv為第一回調參數。

JNIEnv用於線程局部存儲。正出於這個原因,所以不能在線程間共享JNIEnv。如果不能夠通過其它方式獲取其對應的JNIEnv對象,那麼應該先共享JavaVM,然後通過GetEnv函數獲取該線程對應的JNIEnv(假設該線程擁有一個JNIEnv,具體請往下看)。

C與C++對JNIEnv和JavaVM的聲明方式並不相同。頭文件”jni.h”針對C或者C++提供了不同的類型定義。正因為這個原因,在頭文件中包含JNIEnv參數並不是個明智的主意。

線程

Android中所有的線程都是Linux線程,都由內核執行。通常由受控代碼啟動(比如Thread.start),但是也可以由別的地方創建,然後再附加到JavaVM上啟動。舉個例子,線程可以由pthread_create函數創建,然後通過AttachCurrentThread或AttachCurrentThreadAsDaemon將其附加到JavaVM上執行。

Android並不會掛起正在執行本地代碼的線程。如果垃圾收集正在進行,或者調試器發起了掛起請求,那麼Android會在下次JNI調用時暫停線程。

通過JNI所附加的線程在退出前必須調用DetachCurrentThread函數

jclass, jmethodID, 及jfieldID

如果需要在本地代碼中訪問對象的屬性,那麼需要執行以下操作:

通過FindClass獲取類對象的引用 通過GetFieldID獲得屬性的ID 通過對應的方法獲取對象的內容,比如GetIntField

相應的,如果要調用一個方法,首先獲取類對象的引用,其次獲取該方法的ID。ID通常只是指向了一個內部的運行時數據結構。查找這些方法通常需要進行若干次字符串比對,但是一旦找到,那麼後期的獲取屬性或者方法調用都會非常的迅速。

如果性能對你很重要,那麼在找到這些屬性或者方法之後,應該將其緩存起來。因為Android中只允許每個進程有一個JavaVM的存在,所以將這些數據緩存在一個靜態本地結構中是合理的。

類的引用、屬性的ID、方法的ID在這個類被卸載之前都可以保證它們有效。一個類只有在這種情況下才會被卸載:該類所關聯的ClassLoader也能被回收。雖然這幾率很低,但是在Android中不是沒有可能的。

如果想在類加載的時候將這些ID緩存下來,並在類被卸載之後再重新加載時還能重新緩存,最正確的方法是添加這樣一段代碼:

    /*
     * 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。

獲取非局部變量的唯一方式就是通過NewGlobalRef及NewWeakGlobalRef函數獲得。

如果需要長時間持有一段引用,那麼必須使用全局引用。NewGlobalRef函數會將一個局部引用轉換為一個全局引用。在調用DeleteGlobalRef方法之前,該全局引用一直有效。

這種模式通常用於緩存一個由FindClass返回的一個jclass對象:

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

所有的JNI方法都可以以這兩種引用為參數。不過引用相同的值可能有不同的結果。舉個例子,以同一個引用為參數連續調用兩次NewGlobalRef可能會得到不同的值。如果要查看兩個引用是否指向了同一個對象,必須使用IsSameObject函數。絕不要在本地代碼中使用”==”比較兩個引用。

絕不要認為在本地代碼中的對象引用是個常量或者是唯一的。一個32位的值所代表的對象的方法調用可能與下次調用就有所不同,這可能是因為兩個不同的對象擁有相同的32位值。不要將jobject的值當做鍵使用。

程序員經常被要求不要過度的申請局部變量。這意味著如果你創建了大量的局部變量,那麼應當通過DeleteLocalRef函數手動的釋放它們,而不是讓JNI為你做這些事情。

要注意jfieldIDs、jmethodID並不是對象引用,所以不能夠將它們傳給NewGlobalRef函數使用。GetStringUTFChars函數與GetByteArrayElements函數所返回的原始數據指針也同樣不是對象。

一個不尋常的情況需要單獨說明一下:如果通過AttachCurrentThread函數attach到了一個本地線程上,那麼在該線程被detache之前,代碼中所有的局部變量都不會被自動釋放。任何創建的局部變量都需要手動刪除。

UTF-8與UTF-16字符串

Java語言使用的是UTF-16字符串。為了方便起見,JNI所提供的方法工作在Modified UTF-8字符串下。修正後的編碼對於C語言代碼很有用,因為它將\u0000編碼為了0xc0 0x80。

不要忘記釋放你所獲得的字符串。字符串函數會返回jchar* 或 jbyte*,它們是指向原始數據的指針,而不是本地引用。它們在被釋放之前一直有效,這意味著在本地方法返回後,它們並沒有被釋放。

傳給NewStringUTF函數的數據必須是Modified UTF-8格式。一個常見的錯誤就是從文件流或者網絡流中讀取字符串數據,然後沒有過濾就直接交給了NewStringUTF函數進行處理。除非你知道這些數據是7位的ASCII,否則你需要剔除高位的ASCII字符串或者將它們轉換為正確的Modified UTF-8格式。如果你不這麼做,那麼轉換的結果可能不是你想看到的。額外的JNI檢查會掃描字符串並會警告你這是無效的數據,但是它們不會捕獲任何事情。

原始數組

JNI提供了用於訪問對象數組的功能。然而,同一時間只能對一個元素進行訪問,可以直接對數組今夕讀寫操作,就好像直接在C中聲明的一樣。

為了使JNI接口盡可能的高效,也不受虛擬機實現的限制,調用GetArrayElements的相關函數可以返回一個指向實際值的指針,或者可以申請一些內存以完成復制。無論哪種方法,所返回的指針都可以保證是有效的,直到相應的釋放方法被觸發。必須釋放你所取得的每個數組。如果Get方法調取失敗,也需要保證不要去釋放一個空的指針對象。

你可以通過isCopy參數來檢測一個數組是否是由指針所拷貝過來的,這一點很有用。

Release方法需要一個mode參數,這個參數有三種值。運行時執行的操作取決於它返回指向實際數據的指針或者指針的副本:

0
實際指針:非final修飾的數組對象 指針副本:拷貝後的數組數據,拷貝的緩沖區會被釋放 JNI_COMMIT
實際指針:不做任何事情 指針副本:拷貝後的數組數據,拷貝的緩沖區不會被釋放 JNI_ABORT
實際指針:非final修飾的數組對象。早些寫入不會被中止。 指針副本:所拷貝的緩沖區被釋放;緩沖區內的任何變更都會丟失。

檢查isCopy標志的其中一個原因是需要知道在對數組作出變更之後是否需要調用JNI_COMMIT的相關釋放方法,如果要更改一個正在作出變更以及讀取數組內容的操作,那麼可以根據該標志跳過這次操作。另一個可能的原因就是用於有效的處理JNI_ABORT。舉個例子,你可能想要得到一個數組,然後對其修改之後將其傳給一個函數。如果你知道JNI會為你做一個副本的話,那麼就不需要創建另外的可編輯副本了。如果JNI傳回的是原始數據,那麼你自己需要創建一個副本。

一個常見的錯誤就是如果*isCopy是false,那麼可以不調用相關釋放方法。但是事實並非如此,如果沒有申請拷貝緩沖區,那麼原始數據內存必定會被一直占用,也不會被垃圾收集器回收。

還要注意的是,JNI_COMMIT並不會釋放數組,你需要在另外的標志執行後再執行一次釋放。

方法調用

JNI在方法使用上有兩種方式,一種如下所示:

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

上面這段代碼首先得到了一個數組,然後拷貝出len個字節的元素,最後將這個數組釋放。根據實現的不同,Get調用會返回原始數據或者數據副本。在這個案例中,JNI_ABORT可以確保不出現第三個副本。

另一種實現則要更簡單一些:

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

對於此有若干建議:
- 減少JNI調用可以節省開銷。
- 不要原始數據或者額外的數據拷貝。
- 降低程序員出錯的風險–他們會在某些操作失敗後忘記調用相關的釋放方法。

類似的,你可以使用SetArrayRegion函數將數據拷貝到一個數組中,GetStringRegion函數或GetStringUTFRegion可以從String拷貝任意長度的字符。

異常

當異常出現時,請不要繼續向下執行。代碼應當注意到這些異常並返回,或者處理這些異常。

當異常發生時,只有以下JNI方法允許調用:

DeleteGlobalRef DeleteGlobalRef DeleteLocalRef DeleteWeakGlobalRef ExceptionCheck ExceptionClear ExceptionDescribe ExceptionOccurred MonitorExit PopLocalFrame PushLocalFrame ReleaseArrayElements ReleasePrimitiveArrayCritical ReleaseStringChars ReleaseStringCritical ReleaseStringUTFChars

很多JNI函數都會拋出異常,不過只提供了一種很簡單的檢查方法。比如,如果NewString函數返回了一個非空的值,那麼就不需要檢查異常。然而,如果你調用一個方法,比如CallObjectMethod,那麼就需要每次都檢查一下異常,因為如果異常被拋出後,返回值是無效的。

主要注意的是,由中斷所拋出的異常不會釋放本地棧幀,Android目前也不支持C++異常。JNI的Throw與ThrowNew結構也只是在當前的線程設置了一個異常指針。當異常發生時也只是返回到代碼調用處,異常也不會被正確的注意與處理。

本地代碼可以通過ExceptionCheck函數或ExceptionOccurred函數捕獲異常,並可以通過ExceptionClear函數清理這些異常。通常情況下,不處理這些異常會導致一些問題的出現。

JNI中並沒有與Throwable相對應的映射函數,所以,如果你想獲得異常字符串,那麼就需要先找到Throwable類,然後查找相關的getMessage “()Ljava/lang/String;”方法ID,然後調用這些方法,如果返回的值是非空的話,再調用GetStringUTFChars函數來獲得你想得到的異常字符串,最後將這些異常打印出來。

本地庫

你可以通過標准的System.loadLibrary函數加載共享庫中的本地代碼。推薦獲取本地代碼的方法有:

System.loadLibrary(),該方法唯一的參數是一個簡要的庫名,所以如果要加載”libfubar.so”,你只需要傳”fubar”即可。 本地方法:jint JNI_OnLoad(JavaVM* vm, void* reserved); 在JNI_OnLoad方法內部,注冊所有的本地方法。如果將方法聲明為”static”的話,那麼方法名將不會占用符號表的空間。

如果JNI_OnLoad函數是由C++實現的話,那麼它看起來應該是這個樣子:

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast(&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函數外加庫的全限定名來加載本地庫。

使用JNI_OnLoad另一個需要注意的是:任何FindClass調用都會發生在類加載器的上下文環境中,該類加載器用於加載共享庫。通常情況下,FindClass所用到的加載器位於解釋棧的頂端,如果還沒有加載器,那麼它會使用系統的加載器。

64位的注意事項

Android目前運行於32位的平台上。雖然理論上可以為64位的平台構建系統,但是目前它不是主要的目標。大多數情況下,這不是你需要擔心的事情,但是如果要將指針存儲於本地結構中的一個對象的Int屬性上,那麼這就很值得關注了。為了支持64位指針結構,你需要將本地指針存儲於一個Long屬性中

不支持特性與向後兼容

支持所有的JNI1.6特性,以及以下異常:
- DefineClass 還沒有實現。Android並沒有使用Java的字節碼以及類文件,所以傳入二進制的類數據是不會被執行的。

如果需要兼容Android老的版本,那麼應該檢查以下部分:

動態查詢本地函數
在Android 2.0之前,字符’$’在查找方法時不會被正確的轉換為”_00024”。所以使用有關方法需要明確注冊或者將內部類方法移出。 分離線程
在Android 2.0之前,無法使用pthread_key_create析構函數來避免”在退出之前必須分離線程”這項檢查。 弱的全局引用
在Android 2.2之前,弱的全局引用還沒有實現。之前的版本會拒絕使用它們。你可以使用Android平台版本來檢測是否支持。 在Android 4.0之前,弱的全局引用只能被傳入NewLocalRef, NewGlobalRef, 以及 DeleteWeakGlobalRef這幾個函數。 從Android 4.0開始,弱的全局引用可以像其它JNI引用一樣使用。 本地引用
在Android 4.0之前,本地引用實際上就是指針。在Android 4.0之後添加了必要的中間角色,以便更好的支持垃圾回收器的工作,不過這意味著有很多JNI的bug在老版本上無法察覺。查看JNI Local Reference Changes in ICS獲取更多信息。 通過GetObjectRefType檢查引用類型
在Android 4.0之前,由於直接指針的使用,無法正確的實現GetObjectRefType。我們通過弱的全局表、參數、本地表以及全局表進行查找。首先它會找到你的直接指針,並返回它所檢查的引用類型。這意味著,如果你在全局的jclass上作用GetObjectRefType,而這個jclass以一個隱性參數傳給了一個靜態本地方法,那麼你將會獲得JNILocalRefType而不是JNIGlobalRefType。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved