W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
編寫:pedant - 原文:http://developer.android.com/training/articles/perf-jni.html
JNI全稱Java Native Interface。它為托管代碼(使用Java編程語言編寫)與本地代碼(使用C/C++編寫)提供了一種交互方式。它是與廠商無關(guān)的(vendor-neutral),支持從動態(tài)共享庫中加載代碼,雖然這樣會稍顯麻煩,但有時這是相當有效的。
如果你對JNI還不是太熟悉,可以先通讀Java Native Interface Specification這篇文章來對JNI如何工作以及哪些特性可用有個大致的印象。這種接口的一些方面不能立即一讀就顯而易見,所以你會發(fā)現(xiàn)接下來的幾個章節(jié)很有用處。
JNI定義了兩種關(guān)鍵數(shù)據(jù)結(jié)構(gòu),“JavaVM”和“JNIEnv”。它們本質(zhì)上都是指向函數(shù)表指針的指針(在C++版本中,它們被定義為類,該類包含一個指向函數(shù)表的指針,以及一系列可以通過這個函數(shù)表間接地訪問對應(yīng)的JNI函數(shù)的成員函數(shù))。JavaVM提供“調(diào)用接口(invocation interface)”函數(shù), 允許你創(chuàng)建和銷毀一個JavaVM。理論上你可以在一個進程中擁有多個JavaVM對象,但安卓只允許一個。
JNIEnv提供了大部分JNI功能。你定義的所有本地函數(shù)都會接收JNIEnv作為第一個參數(shù)。
JNIEnv是用作線程局部存儲。因此,你不能在線程間共享一個JNIEnv變量。如果在一段代碼中沒有其它辦法獲得它的JNIEnv,你可以共享JavaVM對象,使用GetEnv來取得該線程下的JNIEnv(如果該線程有一個JavaVM的話;見下面的AttachCurrentThread)。
JNIEnv和JavaVM的在C聲明是不同于在C++的聲明。頭文件“jni.h”根據(jù)它是以C還是以C++模式包含來提供不同的類型定義(typedefs)。因此,不建議把JNIEnv參數(shù)放到可能被兩種語言引入的頭文件中(換一句話說:如果你的頭文件需要#ifdef __cplusplus,你可能不得不在任何涉及到JNIEnv的內(nèi)容處都要做些額外的工作)。
所有的線程都是Linux線程,由內(nèi)核統(tǒng)一調(diào)度。它們通常從托管代碼中啟動(使用Thread.start),但它們也能夠在其他任何地方創(chuàng)建,然后連接(attach)到JavaVM。例如,一個用pthread_create啟動的線程能夠使用JNI AttachCurrentThread 或 AttachCurrentThreadAsDaemon函數(shù)連接到JavaVM。在一個線程成功連接(attach)之前,它沒有JNIEnv,不能夠調(diào)用JNI函數(shù)。
連接一個本地環(huán)境創(chuàng)建的線程會觸發(fā)構(gòu)造一個java.lang.Thread對象,然后其被添加到主線程群組(main ThreadGroup),以讓調(diào)試器可以探測到。對一個已經(jīng)連接的線程使用AttachCurrentThread不做任何操作(no-op)。
安卓不能中止正在執(zhí)行本地代碼的線程。如果正在進行垃圾回收,或者調(diào)試器已發(fā)出了中止請求,安卓會在下一次調(diào)用JNI函數(shù)的時候中止線程。
連接過的(attached)線程在它們退出之前必須通過JNI調(diào)用DetachCurrentThread。如果你覺得直接這樣編寫不太優(yōu)雅,在安卓2.0(Eclair)及以上, 你可以使用pthread_key_create來定義一個析構(gòu)函數(shù),它將會在線程退出時被調(diào)用,你可以在那兒調(diào)用DetachCurrentThread (使用生成的key與pthread_setspecific將JNIEnv存儲到線程局部空間內(nèi);這樣JNIEnv能夠作為參數(shù)傳入到析構(gòu)函數(shù)當中去)。
如果你想在本地代碼中訪問一個對象的字段(field),你可以像下面這樣做:
類似地,要調(diào)用一個方法,你首先得獲得一個類對象的引用,然后是方法ID(method ID)。這些ID通常是指向運行時內(nèi)部數(shù)據(jù)結(jié)構(gòu)。查找到它們需要些字符串比較,但一旦你實際去執(zhí)行它們獲得字段或者做方法調(diào)用是非??斓摹?/p>
如果性能是你看重的,那么一旦查找出這些值之后在你的本地代碼中緩存這些結(jié)果是非常有用的。因為每個進程當中的JavaVM是存在限制的,存儲這些數(shù)據(jù)到本地靜態(tài)數(shù)據(jù)結(jié)構(gòu)中是非常合理的。
類引用(class reference),字段ID(field ID)以及方法ID(method ID)在類被卸載前都是有效的。如果與一個類加載器(ClassLoader)相關(guān)的所有類都能夠被垃圾回收,但是這種情況在安卓上是罕見甚至不可能出現(xiàn),只有這時類才被卸載。注意雖然jclass是一個類引用,但是必須要調(diào)用NewGlobalRef保護起來(見下個章節(jié))。
當一個類被加載時如果你想緩存些ID,而后當這個類被卸載后再次載入時能夠自動地更新這些緩存ID,正確做法是在對應(yīng)的類中添加一段像下面的代碼來初始化這些ID:
/*
* 我們在一個類初始化時調(diào)用本地方法來緩存一些字段的偏移信息
* 這個本地方法查找并緩存你感興趣的class/field/method ID
* 失敗時拋出異常
*/
private static native void nativeInit();
static {
nativeInit();
}
在你的C/C++代碼中創(chuàng)建一個nativeClassInit方法以完成ID查找的工作。當這個類被初始化時這段代碼將會執(zhí)行一次。當這個類被卸載后而后再次載入時,這段代碼將會再次執(zhí)行。
每個傳入本地方法的參數(shù),以及大部分JNI函數(shù)返回的每個對象都是“局部引用”。這意味著它只在當前線程的當前方法執(zhí)行期間有效。即使這個對象本身在本地方法返回之后仍然存在,這個引用也是無效的。
這同樣適用于所有jobject的子類,包括jclass,jstring,以及jarray(當JNI擴展檢查是打開的時候,運行時會警告你對大部分對象引用的誤用)。
如果你想持有一個引用更長的時間,你就必須使用一個全局(“global”)引用了。NewGlobalRef函數(shù)以一個局部引用作為參數(shù)并且返回一個全局引用。全局引用能夠保證在你調(diào)用DeleteGlobalRef前都是有效的。
這種模式通常被用在緩存一個從FindClass返回的jclass對象的時候,例如:
jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
所有的JNI方法都接收局部引用和全局引用作為參數(shù)。相同對象的引用卻可能具有不同的值。例如,用相同對象連續(xù)地調(diào)用NewGlobalRef得到返回值可能是不同的。為了檢查兩個引用是否指向的是同一個對象,你必須使用IsSameObject函數(shù)。絕不要在本地代碼中用==符號來比較兩個引用。
得出的結(jié)論就是你絕不要在本地代碼中假定對象的引用是常量或者是唯一的。代表一個對象的32位值從方法的一次調(diào)用到下一次調(diào)用可能有不同的值。在連續(xù)的調(diào)用過程中兩個不同的對象卻可能擁有相同的32位值。不要使用jobject的值作為key.
開發(fā)者需要“不過度分配”局部引用。在實際操作中這意味著如果你正在創(chuàng)建大量的局部引用,或許是通過對象數(shù)組,你應(yīng)該使用DeleteLocalRef手動地釋放它們,而不是寄希望JNI來為你做這些。實現(xiàn)上只預(yù)留了16個局部引用的空間,所以如果你需要更多,要么你刪掉以前的,要么使用EnsureLocalCapacity/PushLocalFrame來預(yù)留更多。
注意jfieldID和jmethodID是映射類型(opaque types),不是對象引用,不應(yīng)該被傳入到NewGlobalRef。原始數(shù)據(jù)指針,像GetStringUTFChars和GetByteArrayElements的返回值,也都不是對象(它們能夠在線程間傳遞,并且在調(diào)用對應(yīng)的Release函數(shù)之前都是有效的)。
還有一種不常見的情況值得一提,如果你使用AttachCurrentThread連接(attach)了本地進程,正在運行的代碼在線程分離(detach)之前決不會自動釋放局部引用。你創(chuàng)建的任何局部引用必須手動刪除。通常,任何在循環(huán)中創(chuàng)建局部引用的本地代碼可能都需要做一些手動刪除。
Java編程語言使用UTF-16格式。為了便利,JNI也提供了支持變形UTF-8(Modified UTF-8)的方法。這種變形編碼對于C代碼是非常有用的,因為它將\u0000編碼成0xc0 0x80,而不是0x00。最愜意的事情是你能在具有C風(fēng)格的以\0結(jié)束的字符串上計數(shù),同時兼容標準的libc字符串函數(shù)。不好的一面是你不能傳入隨意的UTF-8數(shù)據(jù)到JNI函數(shù)而還指望它正常工作。
如果可能的話,直接操作UTF-16字符串通常更快些。安卓當前在調(diào)用GetStringChars時不需要拷貝,而GetStringUTFChars需要一次分配并且轉(zhuǎn)換為UTF-8格式。注意UTF-16字符串不是以零終止字符串,\u0000是被允許的,所以你需要像對jchar指針一樣地處理字符串的長度。
不要忘記Release你Get的字符串。這些字符串函數(shù)返回jchar或者jbyte,都是指向基本數(shù)據(jù)類型的C格式的指針而不是局部引用。它們在Release調(diào)用之前都保證有效,這意味著當本地方法返回時它們并不主動釋放。
傳入NewStringUTF函數(shù)的數(shù)據(jù)必須是變形UTF-8格式。一種常見的錯誤情況是,從文件或者網(wǎng)絡(luò)流中讀取出的字符數(shù)據(jù),沒有過濾直接使用NewStringUTF處理。除非你確定數(shù)據(jù)是7位的ASCII格式,否則你需要剔除超出7位ASCII編碼范圍(high-ASCII)的字符或者將它們轉(zhuǎn)換為對應(yīng)的變形UTF-8格式。如果你沒那樣做,UTF-16的轉(zhuǎn)換結(jié)果可能不會是你想要的結(jié)果。JNI擴展檢查將會掃描字符串,然后警告你那些無效的數(shù)據(jù),但是它們將不會發(fā)現(xiàn)所有潛在的風(fēng)險。
JNI提供了一系列函數(shù)來訪問數(shù)組對象中的內(nèi)容。對象數(shù)組的訪問只能一次一條,但如果原生類型數(shù)組以C方式聲明,則能夠直接進行讀寫。
為了讓接口更有效率而不受VM實現(xiàn)的制約,GetArrayElements系列調(diào)用允許運行時返回一個指向?qū)嶋H元素的指針,或者是分配些內(nèi)存然后拷貝一份。不論哪種方式,返回的原始指針在相應(yīng)的Release調(diào)用之前都保證有效(這意味著,如果數(shù)據(jù)沒被拷貝,實際的數(shù)組對象將會受到牽制,不能重新成為整理堆空間的一部分)。你必須釋放(Release)每個你通過Get得到的數(shù)組。同時,如果Get調(diào)用失敗,你必須確保你的代碼在之后不會去嘗試調(diào)用Release來釋放一個空指針(NULL pointer)。
你可以用一個非空指針作為isCopy參數(shù)的值來決定數(shù)據(jù)是否會被拷貝。這相當有用。
Release類的函數(shù)接收一個mode參數(shù),這個參數(shù)的值可選的有下面三種。而運行時具體執(zhí)行的操作取決于它返回的指針是指向真實數(shù)據(jù)還是拷貝出來的那份。
檢查isCopy標識的一個原因是對一個數(shù)組做出變更后確認你是否需要傳入JNI_COMMIT來調(diào)用Release函數(shù)。如果你交替地執(zhí)行變更和讀取數(shù)組內(nèi)容的代碼,你也許可以跳過無操作(no-op)的JNI_COMMIT。檢查這個標識的另一個可能的原因是使用JNI_ABORT可以更高效。例如,你也許想得到一個數(shù)組,適當?shù)匦薷乃?,傳入部分到其他函?shù)中,然后丟掉這些修改。如果你知道JNI是為你做了一份新的拷貝,就沒有必要再創(chuàng)建另一份“可編輯的(editable)”的拷貝了。如果JNI傳給你的是原始數(shù)組,這時你就需要創(chuàng)建一份你自己的拷貝了。
另一個常見的錯誤(在示例代碼中出現(xiàn)過)是認為當isCopy是false時你就可以不調(diào)用Release。實際上是沒有這種情況的。如果沒有分配備份空間,那么初始的內(nèi)存空間會受到牽制,位置不能被垃圾回收器移動。
另外注意JNI_COMMIT標識沒有釋放數(shù)組,你最終需要使用一個不同的標識再次調(diào)用Release。
當你想做的只是拷出或者拷進數(shù)據(jù)時,可以選擇調(diào)用像GetArrayElements和GetStringChars這類非常有用的函數(shù)。想想下面:
jbyte* data = env->GetByteArrayElements(array, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(array, data, JNI_ABORT);
}
這里獲取到了數(shù)組,從當中拷貝出開頭的len個字節(jié)元素,然后釋放這個數(shù)組。根據(jù)代碼的實現(xiàn),Get函數(shù)將會牽制或者拷貝數(shù)組的內(nèi)容。上面的代碼拷貝了數(shù)據(jù)(為了可能的第二次),然后調(diào)用Release;這當中JNI_ABORT確保不存在第三份拷貝了。
另一種更簡單的實現(xiàn)方式:
env->GetByteArrayRegion(array, 0, len, buffer);
這種方式有幾個優(yōu)點:
類似地,你能使用SetArrayRegion函數(shù)拷貝數(shù)據(jù)到數(shù)組,使用GetStringRegion或者GetStringUTFRegion從String中拷貝字符。
當異常發(fā)生時你一定不能調(diào)用大部分的JNI函數(shù)。你的代碼收到異常(通過函數(shù)的返回值,ExceptionCheck,或者ExceptionOccurred),然后返回,或者清除異常,處理掉。
當異常發(fā)生時你被允許調(diào)用的JNI函數(shù)有:
許多JNI調(diào)用能夠拋出異常,但通常提供一種簡單的方式來檢查失敗。例如,如果NewString返回一個非空值,你不需要檢查異常。然而,如果你調(diào)用一個方法(使用一個像CalllObjectMethod的函數(shù)),你必須一直檢查異常,因為當一個異常拋出時它的返回值將不會是有效的。
注意中斷代碼拋出的異常不會展開本地調(diào)用堆棧信息,Android也還不支持C++異常。JNI Throw和ThrowNew指令僅僅是在當前線程中放入一個異常指針。從本地代碼返回到托管代碼時,異常將會被注意到,得到適當?shù)奶幚怼?/p>
本地代碼能夠通過調(diào)用ExceptionCheck或者ExceptionOccurred捕獲到異常,然后使用ExceptionClear清除掉。通常,拋棄異常而不處理會導(dǎo)致些問題。
沒有內(nèi)建的函數(shù)來處理Throwable對象自身,因此如果你想得到異常字符串,你需要找出Throwable Class,然后查找到getMessage "()Ljava/lang/String;"的方法ID,調(diào)用它,如果結(jié)果非空,使用GetStringUTFChars,得到的結(jié)果你可以傳到printf(3) 或者其它相同功能的函數(shù)輸出。
JNI的錯誤檢查很少。錯誤發(fā)生時通常會導(dǎo)致崩潰。Android也提供了一種模式,叫做CheckJNI,這當中JavaVM和JNIEnv函數(shù)表指針被換成了函數(shù)表,它在調(diào)用標準實現(xiàn)之前執(zhí)行了一系列擴展檢查的。
額外的檢查包括:
(方法和域的可訪問性仍然沒有檢查:訪問限制對于本地代碼并不適用。)
有幾種方法去啟用CheckJNI。
如果你正在使用模擬器,CheckJNI默認是打開的。
如果你有一臺root過的設(shè)備,你可以使用下面的命令序列來重啟運行時(runtime),啟用CheckJNI。
adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
隨便哪一種,當運行時(runtime)啟動時你將會在你的日志輸出中見到如下的字符:
D AndroidRuntime: CheckJNI is ON
如果你有一臺常規(guī)的設(shè)備,你可以使用下面的命令:
adb shell setprop debug.checkjni 1
這將不會影響已經(jīng)在運行的app,但是從那以后啟動的任何app都將打開CheckJNI(改變屬性為其它值或者只是重啟都將會再次關(guān)閉CheckJNI)。這種情況下,你將會在下一次app啟動時,在日志輸出中看到如下字符:
D Late-enabling CheckJNI
你可以使用標準的System.loadLibrary方法來從共享庫中加載本地代碼。在你的本地代碼中較好的做法是:
JNI_OnLoad函數(shù)在C++中的寫法如下:
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
// 使用env->FindClass得到j(luò)class
// 使用env->RegisterNatives注冊本地方法
return JNI_VERSION_1_6;
}
你也可以使用共享庫的全路徑來調(diào)用System.load。對于Android app,你也許會發(fā)現(xiàn)從context對象中得到應(yīng)用私有數(shù)據(jù)存儲的全路徑是非常有用的。
上面是推薦的方式,但不是僅有的實現(xiàn)方式。顯式注冊不是必須的,提供一個JNI_OnLoad函數(shù)也不是必須的。你可以使用基于特殊命名的“發(fā)現(xiàn)(discovery)”模式來注冊本地方法(更多細節(jié)見:JNI spec),雖然這并不可取。因為如果一個方法的簽名錯誤,在這個方法實際第一次被調(diào)用之前你是不會知道的。
關(guān)于JNI_OnLoad另一點注意的是:任何你在JNI_OnLoad中對FindClass的調(diào)用都發(fā)生在用作加載共享庫的類加載器的上下文(context)中。一般FindClass使用與“調(diào)用棧”頂部方法相關(guān)的加載器,如果當中沒有加載器(因為線程剛剛連接)則使用“系統(tǒng)(system)”類加載器。這就使得JNI_OnLoad成為一個查尋及緩存類引用很便利的地方。
Android當前設(shè)計為運行在32位的平臺上。理論上它也能夠構(gòu)建為64位的系統(tǒng),但那不是現(xiàn)在的目標。當與本地代碼交互時,在大多數(shù)情況下這不是你需要擔心的,但是如果你打算存儲指針變量到對象的整型字段(integer field)這樣的本地結(jié)構(gòu)中,這就變得非常重要了。為了支持使用64位指針的架構(gòu),你需要使用long類型而不是int類型的字段來存儲你的本地指針。
除了下面的例外,支持所有的JNI 1.6特性:
對Android以前老版本的向后兼容性,你需要注意:
當使用本地代碼開發(fā)時經(jīng)常會見到像下面的錯誤:
java.lang.UnsatisfiedLinkError: Library foo not found
有時候這表示和它提示的一樣---未找到庫。但有些時候庫確實存在但不能被dlopen(3)找開,更多的失敗信息可以參見異常詳細說明。
你遇到“l(fā)ibrary not found”異常的常見原因可能有這些:
另一種UnsatisfiedLinkError錯誤像下面這樣:
java.lang.UnsatisfiedLinkError: myfunc
at Foo.myfunc(Native Method)
at Foo.main(Foo.java:10)
在日志中,你會發(fā)現(xiàn):
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
這意味著運行時嘗試匹配一個方法但是沒有成功,這種情況常見的原因有:
使用javah來自動生成JNI頭文件也許能幫助你避免這些問題。
確保類名字符串有正確的格式。JNI類名稱以包名開始,然后使用左斜杠來分隔,比如java/lang/String。如果你正在查找一個數(shù)組類,你需要以對應(yīng)數(shù)目的綜括號開頭,使用“L”和“;”將類名兩頭包起來,所以一個一維字符串數(shù)組應(yīng)該寫成[Ljava/lang/String;。
如果類名稱看上去正確,你可能運行時遇到了類加載器的問題。FindClass想在與你代碼相關(guān)的類加載器中開始查找指定的類。檢查調(diào)用堆棧,可能看起像:
Foo.myfunc(Native Method)
Foo.main(Foo.java:10)
dalvik.system.NativeStart.main(Native Method)
最頂層的方法是Foo.myfunc。FindClass找到與類Foo相關(guān)的ClassLoader對象然后使用它。
這通常正是你所想的。如果你創(chuàng)建了自己的線程那么就會遇到麻煩(也許是調(diào)用了pthread_create然后使用AttachCurrentThread進行了連接)。現(xiàn)在跟蹤堆??赡芟裣旅孢@樣:
dalvik.system.NativeStart.run(Native Method)
最頂層的方法是NativeStart.run,它不是你應(yīng)用內(nèi)的方法。如果你從這個線程中調(diào)用FindClass,JavaVM將會啟動“系統(tǒng)(system)”的而不是與你應(yīng)用相關(guān)的加載器,因此試圖查找應(yīng)用內(nèi)定義的類都將會失敗。
下面有幾種方法可以解決這個問題:
也許你會遇到這樣一種情況,想從你的托管代碼或者本地代碼訪問一大塊原始數(shù)據(jù)的緩沖區(qū)。常見例子包括對bitmap或者聲音文件的處理。這里有兩種基本實現(xiàn)方式。
你可以將數(shù)據(jù)存儲到byte[]。這允許你從托管代碼中快速地訪問。然而,在本地代碼端不能保證你不去拷貝一份就直接能夠訪問數(shù)據(jù)。在某些實現(xiàn)中,GetByteArrayElements和GetPrimitiveArrayCritical將會返回指向在維護堆中的原始數(shù)據(jù)的真實指針,但是在另外一些實現(xiàn)中將在本地堆空間分配一塊緩沖區(qū)然后拷貝數(shù)據(jù)過去。
還有一種選擇是將數(shù)據(jù)存儲在一塊直接字節(jié)緩沖區(qū)(direct byte buffer),可以使用java.nio.ByteBuffer.allocateDirect或者NewDirectByteBuffer JNI函數(shù)創(chuàng)建buffer。不像常規(guī)的byte緩沖區(qū),它的存儲空間將不會分配在程序維護的堆空間上,總是可以從本地代碼直接訪問(使用GetDirectBufferAddress得到地址)。依賴于直接字節(jié)緩沖區(qū)訪問的實現(xiàn)方式,從托管代碼訪問原始數(shù)據(jù)將會非常慢。
選擇使用哪種方式取決于兩個方面:
1.大部分的數(shù)據(jù)訪問是在Java代碼還是C/C++代碼中發(fā)生?
2.如果數(shù)據(jù)最終被傳到系統(tǒng)API,那它必須是怎樣的形式(例如,如果數(shù)據(jù)最終被傳到一個使用byte[]作為參數(shù)的函數(shù),在直接的ByteBuffer中處理或許是不明智的)?
如果通過上面兩種情況仍然不能明確區(qū)分的,就使用直接字節(jié)緩沖區(qū)(direct byte buffer)形式。它們的支持是直接構(gòu)建到JNI中的,在未來的版本中性能可能會得到提升。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: