Android筆記 - Dalvik的漫談
hlchou@mail2000.com.tw
by loda
Android/Linux Source Code Tags
App BizOrz
BizOrz.COM
BizOrz Blog
由於Dalvik所涉及的範圍不少,從JNI介面,Trace-JIT的實作,到最佳化的技巧,筆者在本文只會針對自己挑選的區塊以Android 2.2 Source Code加以說明.同樣的,所有涉及的內容,都會隨著Android程式碼的改版而有所差異,還請以最新取得的Package為主.
在此以引用侯捷曾說過的"源碼之前了無秘密",對有志於深入探究Dalvik運作原理的人而言,Android所釋出的Source Code,就是最好的Handbook.
參考Google的文件,我們知道Dalvik是Google在Android手機上所提供的ByteCode虛擬器,所預定的目標是要能運作在效能需求不高,較少的記憶體,與使用慢速的內部Flash儲存,作業系統要能支援虛擬記憶體,執行行程與執行緒(Process/Thread),與具備使用者帳號安全管理機制(UID-based security mechanisms),並能透過GNU C編譯器編譯後運作在包括Linux,BSD與Mac OS X等Unix環境下,並支援Little-endian與Big-endian處理器執行環境.核心的函式庫主要是承襲Open Source的Java SE實作Apache Harmony而來(網址:http://harmony.apache.org/),並基於Open Source中相關OpenSSL, zlib與ICU(International Components for Unicode)的計畫成果.
一般虛擬器的設計,會在應用程式啟動時,動態的從儲存裝置讀取並解壓縮個別的Classes到記憶體中,在沒有經過適度優化的架構下,每個獨立的虛擬器行程,都會包含自己一份的相關Classes記憶體空間,對於程式載入的固定成本與執行效率上,不容易有較好的表現.
因此,參考Google文件,Dalvik設計之初,基於上述的特徵與限制,主要著重在以下的目標,
1,Class Data尤其是共用的ByteCode,要能跨行程共用,節省系統整體記憶體需求(參考ProcessMemory Map (in /proc/xxxx/maps),目前包括相關的 jar與所包含的Bytecode Dex檔案,都能跨行程在不同的Dalvik行程中共用).
2,減少Dalvik應用程式載入啟動的成本,加速應用程式的反應時間
3,Class Data儲存在個別的檔案中,導致額外的儲存成本,針對儲存空間的需求需要多加注意.
4,要從ClassData中讀取出資料數值( 例如:整數或是字串)會增加不必要的成本,評估如何採用C 的形式存取,會是有必要的.
5,ByteCode的驗證雖耗時但卻是必要的,應該試著在程式執行前完成驗證.
6,透過快速指令集與機制的優化進行ByteCode的最佳化對於執行效率與電池壽命是相當重要的.
7,為了安全需求,執行中的行程不能修改共享的程式碼.
基於上述目標,Dalvik在設計時,作了以下的決定
1,可以把多個Classes檔案整合到一個DEX檔案中
2,DEX檔案會以唯讀方案載入到記憶體中,並跨行程共享
3,因應支援的系統架構,調整Byte Ordering與Word Alignment.
4,ByteCode驗證對所有的Classes都是必要的,且會儘可能進行事先的驗證加速系統效率.
5,對於需修改ByteCode的最佳化動作,會在執行前完成.
在dalvik上通常會以.apk的方式來提供應用程式的封裝,或以.jar的方式來提供Framework函式庫,這些包裝格式主要是以zip的形式壓縮內容並加上相關的參考資訊,儲存在檔案系統中,並達到節省儲存空間的目的.也因此,在執行前必須要進行解壓縮的動作,把DEX檔案載入到記憶體中執行. DEX檔案可包括多個Classes檔案,並以classes.dex檔名結尾.
DEX的驗證與最佳化
系統可以在VM進行JIT(Just in time),第一次安裝或在系統編譯時,進行最佳化DEX檔案的動作(ODEX - Optimized DEX),如果是在裝置上進行的最佳化動作,會把最佳化後的檔案存放到dalvik-cache目錄下(例如:/data/dalvik-cache/system@framework@ext.jar@classes.dex),若是在編譯階段的最佳化動作,則是把最佳化後的DEX檔案存放在jar/apk格式的ZIP壓縮檔案中,可成為產品出貨時預設System Image的一部分.
執行時期的dalvik-cache目錄權限為0771並屬於system使用者與system群組(例如:drwxrwx--xsystem system),最佳化後的DEX檔案權限為0644並屬於system使用者與使用者群組(例如:-rw-r--r-- system app_29),而有被DRM-locked保護的應用程式最佳化後的DEX檔案權限會設定為0640以避免其他應用程式取得檔案資料.
AndroidSDK中的System.img所包含的init.rc,在啟動過程中會進行如下的配置
# create dalvik-cache and double-check the perms
mkdir /data/dalvik-cache 0771 system system
chown system system /data/dalvik-cache
chmod 0771 /data/dalvik-cache
最佳化DEX的動作會產生一個可供快速載入執行的classes.dex檔案,並會進行包括byte-swapping,structure realigning與basicstructure checks,更新ODEX(Optimized DEX)header ,為了確保產生ODEX流程的正確性,Android提供了一個dexopt工具(原始碼在dalvik/dexopt),用來作為一個Dalvik虛擬器的輔助工具,可以在系統啟動時,透過Dalvik虛擬器對載入的DEX檔案執行最佳化的動作.
工具dexopt在Android系統中有兩種使用的時機,
1,由Dalvik虛擬器來執行,在有多個Dalvik虛擬器執行的環境中,會透過dexopt locks確保同一個DEX檔案只會執行到一次.
2,在安裝應用程式時,會把檔案從ZIP壓縮中解開,再透過dexopt進行驗證與最佳化
針對DEX檔案所進行的驗證與最佳化程序內容,如下簡介
1,驗證:
會包括DEX檔案中所包括的每個Classes集合以及每個指令集,確保不會在Run-Time階段遇到不適當的指令集,一般而言,Dalvik只有針對被驗證過的Classes進行依據平台的最佳化流程(這條件是可修改的,甚至也可忽略驗證階段只進行最佳化),如果呼叫了一個被驗證失敗的Class所提供的呼叫,就會導致Dalvik虛擬器發生Exception例外, DEX中的Class一旦被驗證過,就會被記錄在ODEX格式的欄位中(包含32-bit check-sum),如此可以避免下次載入時,還要重複進行驗證的工作.
2,最佳化:
會包括把數值資料的定義,透過指標對應到內部的資料結構,把一定成功或是特定行為的指令流程置換為比較簡單的形式,除了必須要在Run-Time才能取得的資訊外,會把可以先決定的資訊先靜態處理完畢.
A,以vtable Index置換虛擬函式(virtual method calls)Index
B,變數的存取,會以實際資料的Byte Offset置換,並且會把資料型別為boolean / byte / char / short 等變數,轉為32bits的形式儲存
C,置換大量調用的函式,例如String.length()透過Inline的置換,直接由Intepreter呼叫到原生函式的實作,減輕函式呼叫的成本,
D,移除空函式的實作,例如Class物件的init空函式,在每次物件被配置時,都還是會被呼叫,就會以nop指令集(0x00)取代.
E,將可以預先計算的資料,進行預處理.
F,會以Dalvik規格中沒有制定的OpCode指令集來進行最佳化,這部份會交由dexopt根據Dalvik虛擬器的版本去決定哪些部分要置換.
G.最佳化的流程,也可以看做是跟ELF執行檔與動態函式庫間解決Symbol Resolve的問題,
上述的最佳化動作,也會使用到來自其他DEX檔案中的Class,如果所參考到的Class資料的內容或是函式有所變動,就會有相依性的問題要被處理,並且要針對變動的部份重新進行最佳化的動作.
DEX 相依性.
由於,最佳化時,會把系統相依的問題進行解決,以加速應用程式載入的效率,也因此,這些在最佳化時所仰賴的相關DEX檔案如果有變動的話,就有機會導致原本最佳化的結果可能造成因為版本差異的所導致的系統問題,也因此,最佳化過的ODEX(Optimized DEX)檔案中會包括所相依的DEX檔案清單以及其CRC-32,時間資訊,對應到dalvik-cache的完整路徑,SHA-1簽名,Dalvik虛擬器版本編號.基於此,如果啟動目錄下的DEX檔案變動,也就象徵著會導致系統上一次相依到該DEX檔案的驗證與最佳化流程,需要被重新執行.如果使用者自定的Class命名與啟動路目錄下的Class名稱重複,系統會對該Class標註並且該參考將不會被驗證與最佳化流程解析.如果DEX檔案在安裝時就是以ODEX的形式安裝,且他所相依的DEX檔案被更新過,這將會導致dexopt沒有辦法對該DEX檔案依據目前系統的狀況進行最佳化產生ODEX,此時Dalvik虛擬器將會拒絕該DEX檔案安裝.
Dalvik雜談
Dalvik虛擬器支援大約230多個指令集OpCode,其中也包含部分由Dexopt插入到ByteCode執行檔中但目前尚未在Android文件”Bytecode for the Dalvik VM” 說明的OpCode指令.
主要的Dalvik虛擬器實作,都是基於C Code,並具有平台移植性,除了JIT(Byte-Code Compiler to ARM/x86 Code)與JNI(JavaNative Interface) Call Bridge有牽涉到跟平台有關的組語優化實作,這會牽涉到包括如何把Byte-Code針對ARMv5,ARMv7,VFP與NEON指令集進行動態的產生與優化,以及由Java端呼叫到Native函式時,要如何根據平台的差異去針對例如C語言的函式參數傳遞進行優化(例如:x86的函式參數傳遞,是由右往左推到Stack中,而ARM的函式參數傳遞,是由左到右依序放到R0,R1,R2,R3通用暫存器),與函式呼叫與傳回值(例如:x86會根據傳回值的Type決定要用EAX或加上EDX暫存器,或是ARM會決定要用到R0或加上R1暫存器).若你所使用的平台,並不在Dalvik JNI Call Bridge支援中,Google文件中也建議可以參考open-source FFI library (A Portable ForeignFunction Interface Library,網址:http://sourceware.org/libffi/),裡面有關於不同平台的可移植函式呼叫實作.對應到Dalvik中這部分的實作位於Source Code的dalvik/vm/arch/目錄下,裡面有關於Java透過JNI機制呼叫到Native Code的實作函式void dvmPlatformInvoke(void* pEnv, ClassObject* clazz, int argInfo,int argc, const u4* argv, const char* signature, void* func, JValue* pReturn) (例如:在x86目錄下實作是在Call386ABI.S與Hints386ABI.c,arm目錄下實作是CallEABI.S,CallOldABI.S與HintsEABI.c,或generic目錄下的實作Call.c與Hints.c),其中,也有包括關於SH處理器Call Bridge的實作,這是由Hitachi與Renesas公司實作後,貢獻到Android計畫中.(ㄟ....那MIPS也應該要這樣子做吧...@_@)
函式dvmPlatformInvoke主要目的是會用來把我們在Java端呼叫JNI函式時所帶入的參數,用C函式參數傳遞的原則進行調整,與處理C函式的回傳值對應回Java的世界中.
Dalvik支援兩種Java虛擬器的直譯器,一個是最早的版本,在文件中稱為Portabl Interpreter,所在原始碼路徑為dalvik/vm/mterp/portable,這個all-in-one-function的C實作,可以用來編譯到不同的平台上,並支援Profiling與除錯機制,根據Config的設定,所在編譯環境會納入編譯的Java虛擬器的直譯器實作會把Source Code放到dalvik/vm/mterp/out目錄下.
Dalvik也支援根據不同平台透過組語優化後的直譯器版本,在文件中稱為Fast Interpreter,可以藉由優化後的平台組語實作,得到更好的Java虛擬器執行效能,在dalvik/vm/mterp/out目錄下可以看到依據平台不同而命名的InterpC-<arch>.c, InterpAsm-<arch>.S原始碼,並可針對不同平台的差異採用不同的優化指令集,例如:在ARM11(ARMv6)架構下可以使用PLD指令,與在ARM7(ARMv4T)架構下要避免使用CLZ指令.
組語版本的優化程式碼,會以64bytes Memory Alignment配置來實作每一個對應的Java指令(也就是說每個Java指令最多可以用16個ARM 32bits指令集實作),如果在Dalvik虛擬器啟動時,發現有對應實作的Java指令超過定義的大小,就會由虛擬器產生錯誤(Abort),參考InterpAsm-armv5te.S,如下指令集實作
.balign64
.L_OP_MOVE_16: /* 0x03 */
/* File: armv5te/OP_MOVE_16.S */
/*for: move/16, move-object/16 */
/*op vAAAA, vBBBB */
FETCH(r1,2) @ r1<- BBBB
FETCH(r0,1) @ r0<- AAAA
FETCH_ADVANCE_INST(3) @ advance rPC, load rINST
GET_VREG(r2,r1) @ r2<- fp[BBBB]
GET_INST_OPCODE(ip) @ extract opcode from rINST
SET_VREG(r2,r0) @ fp[AAAA]<- r2
GOTO_OPCODE(ip) @ jump to next instruction
或
.balign64
.L_OP_MOVE_WIDE: /* 0x04 */
/* File: armv5te/OP_MOVE_WIDE.S */
/*move-wide vA, vB */
/*NOTE: regs can overlap, e.g. "move v6,v7" or "move v7,v6"*/
mov r2, rINST, lsr #8 @ r2<- A(+)
mov r3, rINST, lsr #12 @ r3<- B
and r2, r2, #1
add r3, rFP, r3, lsl #2 @ r3<- &fp[B]
add r2, rFP, r2, lsl #2 @ r2<- &fp[A]
ldmia r3, {r0-r1} @ r0/r1<- fp[B]
FETCH_ADVANCE_INST(1) @ advance rPC, load rINST
GET_INST_OPCODE(ip) @ extract opcode from rINST
stmia r2, {r0-r1} @ fp[A]<- r0/r1
GOTO_OPCODE(ip) @ jump to next instruction
也可以參考網頁http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html,有整理好的Dalvik Java OpCode, 對比Sun Java的OpCode (可以參考網頁:http://java.sun.com/docs/books/jvms/second_edition/html/Mnemonics.doc.html 或是比較完整的文件http://java.sun.com/docs/books/jvms/second_edition/Java5-Instructions2.pdf),對比之後可以知道雖然都是透過JDK編譯的程式碼,透過Google Android Dex轉換後,就會變成Dalvik專屬的OpCode在Dalvik自己的Java虛擬器上執行.
針對使用者所採用平台的組語優化程式碼,可以透過dalvik/vm/mterp/config-<Arch>相關檔案來決定,其中包括要import哪一部分的實作,也規避掉了ARM在不同架構上支援指令集的差異.以dalvik/vm/mterp/config-armv4t為例,擷取其中關於opcode的部分來說明如下
# opcode list; argument to op-start isdefault directory
op-start armv5te
opOP_AGET_WIDE armv4t
opOP_APUT_WIDE armv4t
opOP_IGET_WIDE armv4t
opOP_IGET_WIDE_QUICK armv4t
opOP_IPUT_WIDE armv4t
opOP_IPUT_WIDE_QUICK armv4t
opOP_SGET_WIDE armv4t
opOP_SPUT_WIDE armv4t
op-end
上述宣告,最後產生的dalvik/vm/mterp/out/InterpAsm-armv4t.S除上述AGET_WIDE,APUT_WIDE,IGET_WIDE,IGET_WIDE_QUICK,IPUT_WIDE,IPUT_WIDE_QUICK,SGET_WIDE與SPUT_WIDE指令集會用armv4t的實作外,其他指令集會用armv5te的指令集架構實現.如果有修改上述指令集的實作,會需要執行dalvik/vm/mterp/rebuild.sh,重新產生dalvik/vm/mterp/out/下對應到相關處理器架構的Java指令集實作.並重新編譯dalvik目錄,以便產生對應這次修改的libdvm.so .
Fast Interpreter會是預設的Dalvik虛擬器中的直譯器,如果你所在的處理器沒有Dalvik的組語實作支援,可以由使用者在啟動時選擇要用C Code版本的PortableInterpreter,只要在啟動時(例如在init.rc中)執行 “echo dalvik.vm.execution-mode = int:portable >>/data/local.prop “,就可以選擇採用PortableInterpreter.
一般Java虛擬器直譯器的實作,最直覺的寫法就是用一個很大的Switch-Case,依序對應每個進來的指令集OpCode,做出對應OpCode的行為,在每個指令集OpCode執行完畢後,就回到迴圈的頭,重新Fetch下一次的指令,繼續Siwtch-Case的行為. Google文件中提到的另一種改善的技巧就是透過”threadedexecution”,在每個指令集OpCode執行結束後,進行下一個指令集的fetch與dispatch,省去要回到迴圈啟始點(Branch)的固定成本.
Dalvik直譯器的實作,採用預先算好的Goto位址,基於每個直譯器處理的指令集OpCode實作,都固定以64bytes為Memory Alignment,取代得到一個指令集OpCode後,要進行查表的成本,只要取得OpCode後乘上64bytes(等於2的6次方,可以透過Shift的方式運算),就可以跳到對應指令集OpCode的實作.Android之所以選擇64bytes為每個指令集OpCode實作的最大空間,並沒有特別的理由,主要是這樣的值可以完全適用於目前Android對於ARM與x86上實作的結果.
參考Source Code dalvik/libdex/OpCode.h中的DEFINE_GOTO_TABLE的宣告如下,
#defineDEFINE_GOTO_TABLE(_name) /
static const void*_name[kNumDalvikInstructions] = { /
/* 00..0f */ /
H(OP_NOP), /
H(OP_MOVE), /
H(OP_MOVE_FROM16), /
H(OP_MOVE_16), /
H(OP_MOVE_WIDE), /
H(OP_MOVE_WIDE_FROM16), /
H(OP_MOVE_WIDE_16), /
H(OP_MOVE_OBJECT), /
H(OP_MOVE_OBJECT_FROM16), /
.............................
…....................................etc
再參考Source Code dalvik/vm/mterp/cstubs/entry.c,有如下宣告
/*
* Handler function table, one entry peropcode.
*/
#undefH
#defineH(_op) dvmMterp_##_op
DEFINE_GOTO_TABLE(gDvmMterpHandlers)
#undefH
#defineH(_op) #_op
DEFINE_GOTO_TABLE(gDvmMterpHandlerNames)
定義gDvmMterpHandlers對應到每個OpCode的處理實作,與gDvmMterpHandlerNames對應到每個OpCode的名稱.
再來追蹤函式bool dvmMterpStdRun(MterpGlue* glue) ,可以看到如下的Busy Loop
while(true) {
typedef void (*Handler)(MterpGlue*glue);
u2 inst = /*glue->*/pc[0];
Handler handler = (Handler)gDvmMterpHandlers[inst & 0xff];
LOGVV("handler %p %s/n",
handler, (const char*)gDvmMterpHandlerNames[inst & 0xff]);
(*handler)(glue);
}
會每次取一個Java指令集OpCode,並透過gDvmMterpHandlers對應0x00-0xff範圍的Java指令集OpCode的對應處理函式.在dalvik/vm/mterp/out/InterpAsm-<Arch>.S中,配合Fast Interpreter也有對應依據平台差異的函式dvmMterpStdRun組語版本實作,跟C語言版本實作行為類似,但最大的差異在於,不是透過一個while(true)的Busy Loop去逐一抓取給Dalvik虛擬器的指令集,而是先在函式dvmMterpStdRun中執行如下巨集(以InterpAsm-armv5te.S實作為例)
FETCH_INST() @ load rINST from rPC
GET_INST_OPCODE(ip) @ extract opcode from rINST
GOTO_OPCODE(ip) @ jump to next instruction
抓取第一個Dalvik OpCode指令並執行,同時在每個指令執行結束後,再透過FETCH_ADVANCE_INST與GET_INST_OPCODE抓取下一個Dalvik OpCode指令,最後再透過GOTO_OPCODE執行該指令集的實作,如此持續運作下去,藉此得到比用C版本Busy Loop更高的執行效率.
如果因為進行記憶體的Garbage Collection,進行除錯或是轉換到Native Code執行,而讓執行中的Dalvik行程處於Suspend狀態,Dalvik直譯器會提供一個安全機制,確保行程可以被正確的回復執行.如下所示,目前Dalvik主要支援Portable/Fast Interpreter,而Trace-JIT主要是屬於Fast-Interpreter實作中的一部分.
Trace-JIT/Compiler (For Hot Fragments) | Fast Interpreter (For Cold Fargments) |
Portable Interpreter |
Fast Interpreter (ASM) | ||
Dalvik VM |
Dalvik上每一個應用程式不管是SDK或是NDK都會基於一個Java Based的應用程式為主體(NDK為Java+ELF .so),也因此要了解Android的應用程式Framework,最適當的途徑就是把Dalvik的運作原理與基礎做一個分析,相對這會對於在Android架構下,不管是開發應用程式或是進行系統的效能優化都會有相當的助益.
Dalvik的控制與範例
如果是透過PC上的系統參數設定操作,可以透過adb設定如下的指令
#設定系統參數
adbshell setprop <name> <value>
#取得系統參數
adbshell getprop <name>
相關的Dalvik系統參數設定,是在Zygote行程載入時進行初始化的,一旦相關的系統參數被更動到,就必須要重啟Dalvik Run-Time環境,透過Zygote載入流程讓相關系統參數發揮作用.
在開發階段,有測試系統參數的需求時,就可以藉由Android的Shell,透過Stop/Start指令,終止與重啟Dalvik Run-Time環境,讓系統參數發揮作用.(Stop會依序設定系統參數ctl.stop=runtime與ctl.stop=zygote,Start會依序設定系統參數ctl.start=zygote與ctl.start=runtime)
系統參數 | 值 | 說明 |
dalvik.vm.stack-trace-file | /data/anr/traces.txt | Stack Dumps
Dalvik虛擬器在收到Linux Signal SIGQUIT (也可透過kill -3觸發)就會把目標行程所有執行緒的Stack Traces內容(可以知道每個執行緒函式呼叫的Call Stack),根據這個參數指定的路徑把資料寫入,供開發者分析問題時參考之用. |
dalvik.vm.dexopt-flags | m=y | Bytecode Verification and Optimization 範例:setprop dalvik.vm.dexopt-flags v=a,o=v 用來決定dexopt所進行執行前的驗證與最佳化動作的行為,在實際的裝置上,dexopt會在Dalvik啟動一個應用或是應用程式安裝時調用(會透過dexlock確保同時只有一個被執行到), 進一步說明參數如下 v=a,o=v =>驗證所有的Dex檔案,並且只最佳化被驗證過的Dex檔案,如果驗證失敗,該Dex檔案就不會被執行 v=n,o=v =>關閉驗證Dex的動作,只針對有被驗證過的Dex進行最佳化,沒被驗證過的Dex,就會直接執行(也不會被最佳化)
根據Android的文件,第一次執行的Dex檔案,進行驗證的動作,可能會讓執行時間慢了40%左右,但只要該Dex檔案有驗證過,並且對應放在dalvik-cache下,只要該檔案沒有變動且所相依的其他Dex檔案也沒有更動過,之後執行就可省去驗證的流程,並可透過最佳化機制,加速應用程式的啟動執行. Enabling type-precise GC results in larger optimized DEX files. The additional storage requirements for ".odex" files can cause /system to overflow on some devices, so this is configured separately for each product. |
dalvik.vm.lockprof.threshold | 500 | Enable Dalvik lock contention logging for userdebug builds. |
dalvik.vm.checkjni | true/false | Extended JNI Checks 範例:setprop dalvik.vm.checkjni true 如果系統沒有設置 dalvik.vm.checkjni,就會以ro.kernel.android.checkjni為主,如果有設置dalvik.vm.checkjni(不論是true或false),則以dalvik.vm.checkjni為依據來決定CheckJNI的開或關 另一個對應的參數為 ro.kernel.android.checkjni,可在編譯時期透過build/core/main.mk決定.
這參數的設置,會讓JNI函式呼叫前,執行相關的稽核動作,其中包括 1,查核NULL Pointer 2,查核函式參數的正確性(jclass is a class object, jfieldID points to field data, jstring is a java.lang.String) 3,確認資料寫入動作與變數宣告一致. (例如: don't store a HashMap in a String field.) 4,確認是否有不允許的例外處理有因為函式呼叫正在等待例外的情況. 5,確認關鍵的Get/Release呼叫沒有不適當的函式存在 6,確認JNIEnv不會被跨執行緒(Thread)分享 7,確認Local變數的參考不會超過該變數所能支援的變數壽命週期. 8,UTF-8字串只會包含有效修改的UTF-8資料. |
dalvik.vm.jniopts | forcecopy | Extended JNI Checks 範例:setprop dalvik.vm.jniopts forcecopy
|
dalvik.vm.enableassertions | You can provide a class name, a package name (followed by "..."), or the special value "all" | Assertions 範例: setprop dalvik.vm.enableassertions all
Dalvik虛擬器預設對於Assertion是關閉的,可以透過設定這個參數來開關Dalvik上Java程式碼Assertion的動作, |
dalvik.vm.execution-mode | int:portable/int:fast/int:jit | Execution Mode 範例: setprop dalvik.vm.execution-mode int:portable (C版本的Intepreter) setprop dalvik.vm.execution-mode int:fast (組語版本的Intepreter) setprop dalvik.vm.execution-mode int:jit (Just-in Time加速)
如果有啟動Profiling或是接上Java除錯器,就會切到Debug Mode(我理解Debug Mode是在 C版本的Portable Intepreter中支援的),當除錯階段結束,或是Profiling結束,就會重新回復原本執行的Java模式.
如果在AndroidManifest.xml中把android:vmSafeMode設定為true,就會預設關閉JIT選項,開發者可借此釐清應用程式所造成的問題,是否為JIT所導致的. |
dalvik.vm.deadlock-predict err | off/warn/err | Deadlock Prediction 範例:setprop dalvik.vm.deadlock-predict err
編譯Dalvik時必須要加上WITH_DEADLOCK_PREDICTION便能支援這個功能,參考Android文件,這機制主要用來進行Dalvik上Deadlock的預測,而非偵測
如果設定為off就是關閉,warn是記錄這個問題,但程式繼續執行,如果是err就是在Dalvik OpCpde OP_MONITOR_ENTER(0x1d)指令執行完畢後,立刻觸發Exception,終止Dalvik虛擬器的執行. |
dalvik.vm.check-dex-sum |
| DEX File Checksums 目前Dalvik虛擬器預設對載入ODEX(Optimized DEX)的流程,是不做Checksum確保的,參考dalvik-cache,跟系統與Framework有關的ODEX,所Owner與Group會是root或system,只有一般應用執行的ODEX,會把Group設定為對應的AP Group,基於此,Android預設透過系統存取權限的保護,就可以防止ODEX檔案被修改的可能. 如果所在的儲存媒體不可靠,容易有資料損毀的問題發生,就會有必要加入Checksum的機制(其實以目前很多SmartPhone用MLC NAND+FTL Controller的組合,只要Wear-Leveling有作用,要遇到儲存裝置的”不可靠”,對一般使用者而言,應該是很不容易遇到).開發階段,也可以透過dexdump工具,手動的確認DEX檔案的Checksum. |
dalvik.vm.jit.profile | true/false | JIT Profile 可以用來查看Dalvik JIT對於熱門執行區塊的統計資訊. |
有關Dalvik行程與執行時期介紹
如下所示,為Android系統應用程式啟動的父子行程關係,我們可以看到init會把包括Service Manager,Netd,Rild,MediaServer,BootAnimation這些原生行程載入,而Dalvik的第一個初始化程式會透過app_process載入後,並命名為Zygote(ㄟ...Google翻譯查詢是"受精卵"),再由Zygote帶起system_server與後續的Dalvik應用程式.
Kernel(PID=0) | |||
| /init(PID=1) | /system/bin/sh(PID=27) /system/bin/servicemanager(PID=28) /system/bin/vold(PID=29) /system/bin/netd(PID=30) /system/bin/debuggerd(PID=31) /system/bin/rild(PID=32) /system/bin/mediaserver(PID=34) /system/bin/dbus-daemon(PID=35) /system/bin/installd(PID=36) /system/bin/keystore(PID=37) /system/bin/sh(PID=38) /system/bin/qemud(PID=39) /sbin/adbd(PID=41) /system/bin/bootanimation(PID=81) | |
zygote(PID=33) | system_server (pid:74) | ||
kthreadd(PID=2) | ksoftirqd/0(PID=3) events/0(PID=4) khelper(PID=5) suspend(PID=6) kblockd/0(PID=7) cqueue(PID=8) kseriod(PID=9) kmmcd(PID=10) pdflush(PID=11) pdflush(PID=12) kswapd0(PID=13) aio/0(PID=14) mtdblockd(PID=22) kstriped(PID=23) hid_compat(PID=24) rpciod/0(PID=25) mmcqd(PID=26) |
以目前筆者環境,列舉一個Dalvik應用程式CalendarProvider的記憶體Mapping資訊.對應到其他Dalvik應用程式,我們可以看到基於Apriori prelink機制,在共用原生碼動態函式庫.so的部份,不同的Dalvik應用程式,也會對應到同樣的記憶體位置.而在Dalvik OpCpde編碼動態函式庫有共用的.jar (會被解開為classes.dex)部分,在跨不同的Dalvik應用程式,會被配置到同樣的記憶體位址.
記憶體位址 | 屬性 | 對應的執行檔或是函式庫名稱 |
00008000-00009000 | r-xp | /system/bin/app_process |
00009000-0000a000 | rwxp | /system/bin/app_process |
0000a000-001f9000 | rwxp | [heap] |
40000000-40008000 | r-xs | /dev/ashmem/system_properties |
40008000-40009000 | r-xp |
|
40009000-4024a000 | rwxp | /dev/ashmem/mspace/dalvik-heap/zygote/0 |
4024a000-41009000 | ---p | /dev/ashmem/mspace/dalvik-heap/zygote/0 |
41009000-41038000 | r-xs | /system/fonts/DroidSans.ttf |
41038000-4104c000 | rwxp |
|
4104c000-4104d000 | ---p | /dev/ashmem/dalvik-LinearAlloc |
4104d000-41254000 | rwxp | /dev/ashmem/dalvik-LinearAlloc |
41254000-4154c000 | ---p | /dev/ashmem/dalvik-LinearAlloc |
4154c000-4175b000 | r-xs | /system/framework/core.jar |
4175b000-41c23000 | r-xp | /data/dalvik-cache/system@framework@core.jar@classes.dex |
41c23000-41c68000 | rwxp |
|
41c68000-41ca2000 | r-xs | /system/framework/ext.jar |
41ca2000-41d2e000 | r-xp | /data/dalvik-cache/system@framework@ext.jar@classes.dex |
41d2e000-41fc5000 | r-xs | /system/framework/framework.jar |
41fc5000-425f2000 | r-xp | /data/dalvik-cache/system@framework@framework.jar@classes.dex |
425f2000-42676000 | rwxp |
|
42676000-4268b000 | r-xs | /system/framework/android.policy.jar |
4268b000-426b9000 | r-xp | /data/dalvik-cache/system@framework@android.policy.jar@classes.dex |
426b9000-42752000 | r-xs | /system/framework/services.jar |
42752000-428a0000 | r-xp | /data/dalvik-cache/system@framework@services.jar@classes.dex |
428a0000-428a3000 | rwxp |
|
428a3000-428d9000 | rwxp | /dev/ashmem/dalvik-heap-bitmap/objects |
428d9000-428df000 | rwxp |
|
428df000-428e0000 | r-xs | /dev/ashmem/SurfaceFlinger read-only heap |
428e0000-42907000 | rwxp |
|
42907000-4290c000 | r-xs | /system/app/CalendarProvider.apk |
4291a000-4294b000 | rwxp |
|
4294b000-42982000 | rwxp | /dev/ashmem/dalvik-heap-bitmap/mark/0 |
42982000-429c2000 | rwxp | /dev/ashmem/dalvik-heap-bitmap/mark/1 |
429c2000-42a16000 | r-xs | /system/app/CalendarProvider.apk |
42a16000-42a29000 | r-xs | /system/framework/android.test.runner.jar |
42a29000-42a55000 | r-xp | /data/dalvik-cache/system@framework@android.test.runner.jar@classes.dex |
42a70000-42e23000 | r-xs | /system/framework/framework-res.apk |
42e23000-4304e000 | r-xs | /system/framework/framework-res.apk |
4304e000-4308f000 | rwxp | /dev/ashmem/mspace/dalvik-heap/zygote/1 |
4308f000-43e0e000 | ---p | /dev/ashmem/mspace/dalvik-heap/zygote/1 |
43e0e000-43e4f000 | rwxp | /dev/ashmem/mspace/dalvik-heap/2 |
43e4f000-44b8e000 | ---p | /dev/ashmem/mspace/dalvik-heap/2 |
44b8e000-44b8f000 | ---p |
|
44b8f000-44c8e000 | rwxp |
|
44c8e000-44c8f000 | ---p |
|
44c8f000-44d8e000 | rwxp |
|
44d8e000-44e8c000 | r-xp | /dev/binder |
44e8c000-44e8d000 | ---p |
|
44e8d000-44f8c000 | rwxp |
|
44f8c000-44f8d000 | ---p |
|
44f8d000-4508c000 | rwxp |
|
4508c000-450e0000 | r-xs | /system/app/CalendarProvider.apk |
450e0000-451ab000 | r-xp | /data/dalvik-cache/system@app@CalendarProvider.apk@classes.dex |
80000000-801d0000 | r-xp | /system/lib/libicudata.so |
801d0000-801d1000 | rwxp | /system/lib/libicudata.so |
80200000-80286000 | r-xp | /system/lib/libdvm.so |
80286000-80289000 | rwxp | /system/lib/libdvm.so |
80289000-8028a000 | rwxp |
|
9d100000-9d139000 | r-xp | /system/lib/libstlport.so |
9d139000-9d13b000 | rwxp | /system/lib/libstlport.so |
9d700000-9d736000 | r-xp | /system/lib/libjpeg.so |
9d736000-9d737000 | rwxp | /system/lib/libjpeg.so |
9ea00000-9ea08000 | r-xp | /system/lib/libdrm1.so |
9ea08000-9ea09000 | rwxp | /system/lib/libdrm1.so |
a2f00000-a2fa9000 | r-xp | /system/lib/libstagefright.so |
a2fa9000-a2fac000 | rwxp | /system/lib/libstagefright.so |
a3500000-a3503000 | r-xp | /system/lib/libstagefright_color_conversion.so |
a3503000-a3504000 | rwxp | /system/lib/libstagefright_color_conversion.so |
a3600000-a3605000 | r-xp | /system/lib/libstagefright_avc_common.so |
a3605000-a3606000 | rwxp | /system/lib/libstagefright_avc_common.so |
a3700000-a370c000 | r-xp | /system/lib/libstagefright_amrnb_common.so |
a370c000-a370d000 | rwxp | /system/lib/libstagefright_amrnb_common.so |
a3900000-a39c7000 | r-xp | /system/lib/libopencore_common.so |
a39c7000-a39cd000 | rwxp | /system/lib/libopencore_common.so |
a4800000-a48b6000 | r-xp | /system/lib/libopencore_player.so |
a48b6000-a48be000 | rwxp | /system/lib/libopencore_player.so |
a5900000-a5917000 | r-xp | /system/lib/libomx_amrenc_sharedlibrary.so |
a5917000-a5918000 | rwxp | /system/lib/libomx_amrenc_sharedlibrary.so |
a6800000-a682f000 | r-xp | /system/lib/libopencore_net_support.so |
a682f000-a6832000 | rwxp | /system/lib/libopencore_net_support.so |
a6d00000-a6d14000 | r-xp | /system/lib/libomx_sharedlibrary.so |
a6d14000-a6d15000 | rwxp | /system/lib/libomx_sharedlibrary.so |
a7500000-a7502000 | r-xp | /system/lib/libemoji.so |
a7502000-a7503000 | rwxp | /system/lib/libemoji.so |
a7e00000-a7e05000 | r-xp | /system/lib/libhardware_legacy.so |
a7e05000-a7e06000 | rwxp | /system/lib/libhardware_legacy.so |
a7f00000-a7f01000 | r-xp | /system/lib/libhardware.so |
a7f01000-a7f02000 | rwxp | /system/lib/libhardware.so |
a8100000-a8124000 | r-xp | /system/lib/libutils.so |
a8124000-a8125000 | rwxp | /system/lib/libutils.so |
a8200000-a821f000 | r-xp | /system/lib/libbinder.so |
a821f000-a8225000 | rwxp | /system/lib/libbinder.so |
a8300000-a86ec000 | r-xp | /system/lib/libwebcore.so |
a86ec000-a8747000 | rwxp | /system/lib/libwebcore.so |
a8747000-a8749000 | rwxp |
|
a8a00000-a8a14000 | r-xp | /system/lib/libexpat.so |
a8a14000-a8a16000 | rwxp | /system/lib/libexpat.so |
a8b00000-a8b4d000 | r-xp | /system/lib/libsqlite.so |
a8b4d000-a8b4f000 | rwxp | /system/lib/libsqlite.so |
a9000000-a9051000 | r-xp | /system/lib/libmedia.so |
a9051000-a905d000 | rwxp | /system/lib/libmedia.so |
a9300000-a930c000 | r-xp | /system/lib/libmedia_jni.so |
a930c000-a930d000 | rwxp | /system/lib/libmedia_jni.so |
a9400000-a941c000 | r-xp | /system/lib/libvorbisidec.so |
a941c000-a941d000 | rwxp | /system/lib/libvorbisidec.so |
a9500000-a9552000 | r-xp | /system/lib/libsonivox.so |
a9552000-a9553000 | rwxp | /system/lib/libsonivox.so |
a9553000-a9554000 | rwxp |
|
a9c00000-a9c0a000 | r-xp | /system/lib/libskiagl.so |
a9c0a000-a9c0b000 | rwxp | /system/lib/libskiagl.so |
ab100000-ab211000 | r-xp | /system/lib/libskia.so |
ab211000-ab215000 | rwxp | /system/lib/libskia.so |
ab215000-ab218000 | rwxp |
|
ab900000-ab912000 | r-xp | /system/lib/libui.so |
ab912000-ab914000 | rwxp | /system/lib/libui.so |
aba80000-aba90000 | r-xp | /system/lib/libcamera_client.so |
aba90000-aba93000 | rwxp | /system/lib/libcamera_client.so |
abb00000-abb09000 | r-xp | /system/lib/libexif.so |
abb09000-abb0a000 | rwxp | /system/lib/libexif.so |
abb0a000-abb0c000 | rwxp |
|
abd00000-abd02000 | r-xp | /system/lib/libETC1.so |
abd02000-abd03000 | rwxp | /system/lib/libETC1.so |
abe00000-abe08000 | r-xp | /system/lib/libEGL.so |
abe08000-abe09000 | rwxp | /system/lib/libEGL.so |
abe09000-abe0b000 | rwxp |
|
ac100000-ac104000 | r-xp | /system/lib/libGLESv2.so |
ac104000-ac105000 | rwxp | /system/lib/libGLESv2.so |
ac200000-ac205000 | r-xp | /system/lib/libGLESv1_CM.so |
ac205000-ac206000 | rwxp | /system/lib/libGLESv1_CM.so |
ac700000-ac715000 | r-xp | /system/lib/libsurfaceflinger_client.so |
ac715000-ac718000 | rwxp | /system/lib/libsurfaceflinger_client.so |
ac900000-ac919000 | r-xp | /system/lib/libpixelflinger.so |
ac919000-ac91b000 | rwxp | /system/lib/libpixelflinger.so |
ad100000-ad12f000 | r-xp | /system/lib/libnativehelper.so |
ad12f000-ad132000 | rwxp | /system/lib/libnativehelper.so |
ad300000-ad36d000 | r-xp | /system/lib/libandroid_runtime.so |
ad36d000-ad375000 | rwxp | /system/lib/libandroid_runtime.so |
ad375000-ad37a000 | rwxp |
|
ad900000-ad9e0000 | r-xp | /system/lib/libicui18n.so |
ad9e0000-ad9e4000 | rwxp | /system/lib/libicui18n.so |
ad9e4000-ad9e5000 | rwxp |
|
ade00000-aded0000 | r-xp | /system/lib/libicuuc.so |
aded0000-aded8000 | rwxp | /system/lib/libicuuc.so |
aded8000-adeda000 | rwxp |
|
ae300000-ae304000 | r-xp | /system/lib/libnetutils.so |
ae304000-ae305000 | rwxp | /system/lib/libnetutils.so |
ae400000-ae402000 | r-xp | /system/lib/libwpa_client.so |
ae402000-ae403000 | rwxp | /system/lib/libwpa_client.so |
af000000-af08d000 | r-xp | /system/lib/libcrypto.so |
af08d000-af09f000 | rwxp | /system/lib/libcrypto.so |
af09f000-af0a1000 | rwxp |
|
af400000-af424000 | r-xp | /system/lib/libssl.so |
af424000-af427000 | rwxp | /system/lib/libssl.so |
af700000-af713000 | r-xp | /system/lib/libz.so |
af713000-af714000 | rwxp | /system/lib/libz.so |
af900000-af90e000 | r-xp | /system/lib/libcutils.so |
af90e000-af90f000 | rwxp | /system/lib/libcutils.so |
af90f000-af91e000 | rwxp |
|
afa00000-afa03000 | r-xp | /system/lib/liblog.so |
afa03000-afa04000 | rwxp | /system/lib/liblog.so |
afb00000-afb20000 | r-xp | /system/lib/libm.so |
afb20000-afb21000 | rwxp | /system/lib/libm.so |
afc00000-afc01000 | r-xp | /system/lib/libstdc++.so |
afc01000-afc02000 | rwxp | /system/lib/libstdc++.so |
afd00000-afd3f000 | r-xp | /system/lib/libc.so |
afd3f000-afd42000 | rwxp | /system/lib/libc.so |
afd42000-afd4d000 | rwxp |
|
b0001000-b000c000 | r-xp | /system/bin/linker |
b000c000-b000d000 | rwxp | /system/bin/linker |
b000d000-b0016000 | rwxp |
|
bea87000-bea9c000 | rwxp | [stack] |
|
|
|
|
|
|
0xc0000000--------- |
| LINUX KERNEL |
|
|
|
使用 ANT包裝NDK應用程式為APK
Java的應用,通常可以透過Ant來包裝動態函式庫.so檔案與Java應用程式為.apk,Ant是一個Apache計畫的產物,官方網站為http://ant.apache.org/ ,下載JUnit https://github.com/KentBeck/junit/downloads ( 筆者下載的路徑是http://cloud.github.com/downloads/KentBeck/junit/junit-4.9b2.jar),把junit-4.9b2.jar複製到ant解開後的路徑 lib/optional下,然後如下設定環境變數
(http://ant.apache.org/manual/index.html)
exportANT_HOME=/usr/local/ant
exportJAVA_HOME=/android/jdk1.5.0_22 (如果是Cygwin就設定到JDK安裝的路徑,例如export JAVA_HOME=/cygdrive/c/"ProgramFiles"/Java/jdk1.6.0_23)
export PATH=${PATH}:${ANT_HOME}/bin
與
NDK=/android-ndk-r5b-windows
接下來,執行./build.sh
就可以產生ANT執行的環境,以NDK為例,可以進到範例目錄
透過 Android SDK tools下的android.bat產生讓ANT參考的build.xml
snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni
$ android.batupdate project -p . -s
Updated local.properties
Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/build.xml
Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/proguard.cfg
Updated local.properties
Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/tests/build.xml
Added fileC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/tests/proguard.cfg
透過 ndk-build進行編譯
snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni
$ $NDK/ndk-build
(要編譯Debug版本就是加上NDK_LOG=1NDK_DEBUG=1)
Gdbserver : [arm-linux-androideabi-4.4.3]libs/armeabi/gdbserver
Gdbsetup : libs/armeabi/gdb.setup
Install : libhello-jni.so =>libs/armeabi/libhello-jni.so
透過ANT產生APK
snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni
$ ant debug
Buildfile: C:/cygwin/android-ndk-r5b-windows/samples/hello-jni/build.xml
[setup]Android SDK Tools Revision 8
[setup]Project Target: Android 2.2
….......................
debug:
[echo]Running zip align on final apk...
[echo]Debug Package: C:/cygwin/android-ndk-r5b-windows/samples/hello-jni/bin/HelloJni-debug.apk
BUILD SUCCESSFUL
Total time: 5 seconds
安裝 APK到android手機環境
snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni
$ adb installbin/HelloJni-debug.apk (如果之前有安裝過,可以加上-r,重新安裝)
396 KB/s (79155 bytes in 0.195s)
pkg: /data/local/tmp/HelloJni-debug.apk
Success
安裝完畢後,如果希望透過ndk-gdb除錯時,可以在程式啟動後,立刻停住等待除錯器啟動,不要就直接執行下去,可以修改Java程式碼加入函式呼叫android.os.Debug.waitForDebugger() , 如下所示,修改/android-ndk-r5b-windows/samples/hello-jni/src/com/example/hellojni/HelloJni.java
package com.example.hellojni;
import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;
public class HelloJni extends Activity
{
/** Called when the activityis first created. */
@Override
public void onCreate(BundlesavedInstanceState)
{
android.os.Debug.waitForDebugger();
super.onCreate(savedInstanceState);
snna@snna-PC/android-ndk-r5b-windows/samples/hello-jni
$ $NDK/ndk-gdb--start
GNU gdb 6.6
Copyright (C) 2006 Free SoftwareFoundation, Inc.
GDB is free software, covered by the GNUGeneral Public License, and you are
welcome to change it and/or distributecopies of it under certain conditions.
Type "show copying" to see theconditions.
There is absolutely no warranty forGDB. Type "show warranty" fordetails.
This GDB was configured as"--host=i586-mingw32msvc --target=arm-elf-linux".
(no debugging symbols found)
Android Runtime
包括工具dexopt,或是app_process(zygote)與dalvikvm這些在Android目錄下可以使用的執行檔,其實最主要的實作都是基於/system/lib/libdvm.so,並且透過JNI串起原生程式碼與Java程式碼的介面,這些工具主要是把要傳遞給libdvm.so處理的資料,進行前段的處理,如下以app_process為例說明Zygote程序的初始化流程.
Dalvik第一個Java應用程式Zygote,就由此而來,app_process Source Code所在路徑為frameworks/base/cmds/app_process.系統在初始化Zygote時,會帶入如下的參數
/system/bin/app_process -Xzygote/system/bin --zygote –start-system-server
由於最終實作的內容是在Java Framework中的,com.android.internal.os.ZygoteInit (原始碼路徑:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java),在app_process的C實作中,主要是初始化Dalvik虛擬器,並且把要帶入Java的資訊加以處理,我們簡要說明如下.
1,在C程式碼中產生AndroidRuntime (AndroidRuntime實作在frameworks/base/core/jni/AndroidRuntime.cpp)
2,呼叫AndroidRuntime::addVmArguments把C參數的個數與內容扣掉屬於app_process的部份(個數減一與字串往前移動一).傳給AndroidRuntime
3,如果參數等於--zygote與隨後參數等於--start-system-server就把變數startSystemServer設為true,並呼叫set_process_name設定行程名稱為zygote,再來執行AndroidRuntime::start(constchar* className, const bool startSystemServer)函式,該函式會初始化Dalvik虛擬器(由AndroidRuntime::startVm呼叫函式JNI_CreateJavaVM(原始碼在:dalvik/vm/Jni.c)),並由C透過JNI介面(FindClass,GetStaticMethodID and CallStaticVoidMethod)執行Java的com.android.internal.os.ZygoteInit Class(原始碼路徑:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java)的ZygoteInit::main函式,執行如下行為(只列舉筆者覺得重要的)
3.a,註冊ZygoteSocket
3.b,Preload所需的DEX/Class檔案,例如:會透過dvmJarFileOpen(原始碼在:dalvik/vm/JarFile.c),解開ZIP壓縮的JAR檔案,先確認是否有odex結尾的檔案(表示已經做過最佳化的動作),如果有,就會確認該odex檔案的Class相依性(如果有相依的Class被更新,就要重新進行最佳化),接下來搜尋是否有classes.dex結尾的檔案(尚未被最佳化過),並呼叫dvmOptimizeDexFile(原始碼在:dalvik/vm/analysis/DexOptimize.c)進行驗證與最佳化DEX的流程.
3.c,進行初始化後的記憶體回收動作(GC,Memory Garbage Collection)
3.d,如果startSystemServer為true,呼叫startSystemServer產生System Server行程.
3.e,Zygote進入一個Socket等待Client連結的無窮迴圈.至此即完成zygote行程的初始化動作.
4,如果參數不為--zygote,就會把Class名稱與C參數的個數與內容扣掉屬於app_process的部份(個數減一與字串往前移動一).傳給AndroidRuntime.並呼叫set_process_name設定行程為該Class名稱,再來執行AndroidRuntime::start(const char* className, const boolstartSystemServer)函式,該函式會初始化Dalvik虛擬器,並由C透過JNI介面執行Java的com.android.internal.os.RuntimeInit Class(原始碼路徑:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java)的RuntimeInit::main函式
JNI (JavaNative Interface)
JNI的介面是原本Java環境中就有的機制,讓Java的應用程式可以跟其他語言實作的動態函式庫進行互通,更進一步來說,就是可以透過Native Code執行的高效率,優化Java應用程式執行上表現.
有兩份關於JNI的文件,是Google推薦開發者可以參考的
1,Introductionand Tutorial
(http://java.sun.com/docs/books/jni/html/jniTOC.html)
2,Java Native Interface Specification for J2SE 1.6
(http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html)
JNI的C函式可以存取"JavaVM"或"JNIEnv"的資料結構,一個JNI C函式的第一個函式參數會是JNIEnv (例如:HelloWorld(JNIEnv * env, jobject jobj)),並且以Dalvik的設計而言,是以一個JavaAP對應到一個Linux Process並對應到一個Dalvik VM的實體,也就是說,Dalvik VM本身並不會進行MultiTask Java AP的動作,同時,每個Java AP與其對應的JNI C動態函式庫,就會以一個Linux Process行程空間(每個都是一個獨立的4GB記憶體空間)為單位來運作.
JNI可以透過FindClass取得Java世界所宣告的Class,透過GetFieldID取得Java世界的變數(並且也可以改變,設定值與使用該變數),與可以透過GetMethodID取得Java世界的Class函式呼叫.有關字元部分需要注意的是,Java世界採用的字元是以Unicode(UTF-16 LE)編碼,而在JNI的C世界中是採用UTF-8的方式編碼.
以c為例,在實作JNI的函式時,所面對的資料型態,主要是以j開頭加上對應的Java端的資料型態,例如
Java中的型態 | JNI 中的型態 | 對應到C實作中的型態 |
boolean | jboolean | unsigned char ( unsigned 8 bits) |
byte | jbyte | signed char (signed 8 bits) |
char | jchar | unsigned short (unsigned 16 bits) |
short | jshort | short (signed 16 bits) |
int | jint | int (signed 32 bits) |
long | jlong | long long (signed 64 bits) |
float | jfloat | float (32-bit IEEE 754) |
double | jdouble | double ( 64-bit IEEE 754) |
size | jsize | int (signed 32 bits) |
void | void | void |
String | jstring | void * |
object | jobject | void * |
class | jclass | void * |
array | jarray | void * |
| jobjectArray/jbooleanArray/jbyteArray/jcharArray/jshortArray/jintArray/jlongArray/jfloatArray/jdoubleArray/jthrowable/jweak | void * |
需要注意的是,在Java中字元是以Unicode的方式編碼,所以jchar對應一個Java字元,才會等於一個unsignedshort 16bits字元,但是在JNI這邊C語言的實作則是以UTF-8的方式來實現,也就是說ASCII字元固定為1個bytes,但中文或其他語系的編碼就會有2-6bytes不等的編碼長度(可以參考http://en.wikipedia.org/wiki/UTF-8),中文等亞洲語系通常為3bytes.
如下,提供一個簡單沒有傳入參數與傳回值的JNI範例libtest8.c
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define LOG_TAG "C LOG DEMO"
#undef LOG
#include <utils/Log.h>
JNIEXPORT void JNICALLJava_loda_Test8_Test8_HelloWorld(JNIEnv * env, jobject jobj)
{
printf("Hello World in Console./n");
LOGV("Hello World! This color is for Verbose.");
LOGD("Hello World! This color is for Debug.");
LOGI("Hello World! This color is for Information");
LOGW("Hello World! This color is for Warnning.");
LOGE("Hello World! This color is for Error.");
}
透過如下內容的Android.mk
LOCAL_PATH := $(my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := libtest8.c
LOCAL_SHARED_LIBRARIES := liblog
LOCAL_MODULE := libtest8
LOCAL_PRELINK_MODULE := false
include $(BUILD_SHARED_LIBRARY)
之後執行
[root@localhost froyo]# make libtest8
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=2.2.1
TARGET_PRODUCT=generic
TARGET_BUILD_VARIANT=eng
TARGET_SIMULATOR=
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=arm
HOST_ARCH=x86
HOST_OS=linux
HOST_BUILD_TYPE=release
BUILD_ID=MASTER
============================================
target thumb C: libtest8 <=system/core/libtest8/libtest8.c
target SharedLib: libtest8(out/target/product/generic/obj/SHARED_LIBRARIES/libt
est8_intermediates/LINKED/libtest8.so)
target Non-prelinked: libtest8(out/target/product/generic/symbols/system/lib/li
btest8.so)
target Strip: libtest8(out/target/product/generic/obj/lib/libtest8.so)
Install:out/target/product/generic/system/lib/libtest8.so
之後,筆者把libtest.so放到Windows環境下,首先原本的system.img载入是唯讀的,必須要透過
adb reomunt
讓system.img可以寫入資料(/dev/block/mtdblock0 /system yaffs2 rw 0 0)
之後,透過adb push libtest8.so /system/lib
616 KB/s (25248 bytes in 0.040s)
把函式庫放到目標主機上,然後透過Eclipse執行下面的程式碼
package loda.Test8;
import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
import java.io.IOException;
import java.io.InputStream;
import java.lang.IllegalStateException;
public class Test8 extends Activity {
private native voidHelloWorld();
static {
System.loadLibrary("test8");
}
@Override
public void onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("loda","libtest8 Before Call JNI HelloWorld");
HelloWorld();
Log.i("loda","libtest8 After Call JNI HelloWorld");
setContentView(R.layout.main);
}
}
就可以看到在Eclipse的環境中,的logcat有來自Java程式透過JNI介面呼叫HelloWorld C函式庫的結果了.
接下來,我們把這個JNI範例加上傳入參數與傳回值,如下範例C程式
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define LOG_TAG "C LOG DEMO"
#undef LOG
#include <utils/Log.h>
char gTest8Buffer[]="Unicode basedGlobal Sring Reference from JNI-C";
JNIEXPORT jstring JNICALLJava_loda_Test8d_Test8d_HelloWorld(JNIEnv * env, jobject jobj,jinttest_int,jstring test_string,jdouble test_double)
{
const jbyte *jni_string;
jni_string=(*env)->GetStringUTFChars(env,test_string,0);
if(jni_string==NULL)
{
return NULL;
}
printf("int:%d string:%sdouble:%f/n",test_int,jni_string,test_double);
LOGI("int:%d string:%s double:%f/n",test_int,jni_string,test_double);
(*env)->ReleaseStringUTFChars(env,test_string,jni_string);
return (*env)->NewStringUTF(env, gTest8Buffer);
}
我們額外加入, int,String與double型態的參數,由Java端傳入給JNI的C程式,編譯後,同樣的透過adb push libtest8d.so /system/lib
61 KB/s (5164 bytes in 0.082s)
傳到Android Target上,然後再Eclipse上編譯如下的Java程式
package loda.Test8d;
import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
public class Test8d extends Activity {
private native String HelloWorld(int a,Stringb,double c);
static {
System.loadLibrary("test8d");
}
@Override
public void onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("loda","libtest8d Before Call JNI HelloWorld");
String vStr="It is a stringfrom Java";
String vRet=HelloWorld(5799232,vStr,123.456);
Log.i("loda",vRet);
Log.i("loda","libtest8d After Call JNI HelloWorld");
setContentView(R.layout.main);
}
}
就可以示範如何從Java端傳遞整數,字串與浮點數給JNI C程式,然後再從C程式中傳遞結果字串給Java應用程式.
接下來,我們再舉另一個例子,示範如何從JNI C程式中,去呼叫Java端的應用函式,如此我們就可以達成C與Java端彼此可以呼叫的結果,並且可以驗證這些JNI的行為在Android架構下確實可以運作無誤.
(參考:http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html)
Signature | Java Type |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L | fully-qualified-class; |
[ type | type[] |
( arg-types ) ret-type | method type |
如下所示,我們在JNI C函式中透過GetMethodID取得Java函式String CallFromJNI(int a,String b,double c)的c函式與參數宣告 “(ILjava/lang/String;D)Ljava/lang/String;”,
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define LOG_TAG "C LOG DEMO"
#undef LOG
#include <utils/Log.h>
char gTest8Buffer[]="Unicode basedGlobal Sring Reference from JNI-C";
JNIEXPORT jstring JNICALLJava_loda_Test8d_Test8d_HelloWorld(JNIEnv * env, jobject jobj,jinttest_int,jstring test_string,jdouble test_double)
{
jclass vClass=(*env)->GetObjectClass(env,jobj);
jmethodID vMethodID=(*env)->GetMethodID(env,vClass,"CallFromJNI","(ILjava/lang/String;D)Ljava/lang/String;");
if(vMethodID==NULL)
{
LOGI("Falied to load JavaFunc CallFromJNI");
return NULL;
}
jstring vStr=(*env)->NewStringUTF(env, gTest8Buffer);
jstringvRetStr=(*env)->CallObjectMethod(env,jobj,vMethodID,123,vStr,4321.1234);
const jbyte *jni_string=(*env)->GetStringUTFChars(env,vRetStr,0);
if(jni_string==NULL)
{
return NULL;
}
LOGI("Java Method Return String:%s",jni_string);
(*env)->ReleaseStringUTFChars(env,vRetStr,jni_string);
return vStr;
透過如下命令傳到手機上
adb push libtest8d.so /system/lib
419 KB/s (5160 bytes in 0.012s)
再透過如下的Java 程式碼,就可以完成從Java呼叫JNI C函式,到由JNI C函式呼叫回Java函式的兩者互通的函式呼叫機制,並可在Android環境中驗證無誤,如下範例程式碼
package loda.Test8d;
import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
public class Test8d extends Activity {
private native String HelloWorld(int a,Stringb,double c);
static {
System.loadLibrary("test8d");
}
private String CallFromJNI(int a,Stringb,double c)
{
Log.i("loda","int:"+a+"_String:"+b+"_double:"+c);
return "From CallFromJNI in Java";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Log.i("loda","libtest8d Before Call JNI HelloWorld");
String vStr="It is a stringfrom Java";
String vRet=HelloWorld(5799232,vStr,123.456);
Log.i("loda", vRet);
Log.i("loda","libtest8d After Call JNI HelloWorld");
setContentView(R.layout.main);
}
}
最後,再示範如何透過JNI C函式,去取得Java的Class變數的機制,雖然一般模組化的設計,跨模塊的變數存取最好都是透過函式實作來達成,直接去存取變數並不是一個好的實作方式,但本文的目的主要是演示這些JNI操作的實例作為相關設計的參考之用,所以,還請僅供各位參考,實作上還是建議可以透過函式來使用跨模組的變數,而不是直接自行存取造成後續維護或是問題收斂上的困難.
如下為JNI C程式碼的範例,由C程式碼去參考Java程式碼中宣告的int,String與double變數
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define LOG_TAG "C LOG DEMO"
#undef LOG
#include <utils/Log.h>
char gTest8Buffer[]="Sring fromJNI-C";
JNIEXPORT jstring JNICALLJava_loda_Test8e_Test8e_HelloWorld(JNIEnv * env, jobject jobj)
{
jfieldIDvFieldID;
jintjInt;
jstringjStr;
jdoublejDouble;
constchar *vpTemp;
jclassvClass=(*env)->GetObjectClass(env,jobj);
/
//Getand Set Integer from Java
//
vFieldID=(*env)->GetFieldID(env,vClass,"IntInJava","I");
if(vFieldID==NULL)
{
LOGI("Faliedto load Java Integer Variable");
returnNULL;
}
jInt=(*env)->GetIntField(env,jobj,vFieldID);
if(jInt==0)
{
LOGI("Faliedto get jInt Object");
returnNULL;
}
LOGI("jInt:%d",jInt);
jInt=987654321;
(*env)->SetIntField(env,jobj,vFieldID,jInt);
/
/
//Getand Set String from Java
//
vFieldID=(*env)->GetFieldID(env,vClass,"StringInJava","Ljava/lang/String;");
if(vFieldID==NULL)
{
LOGI("Faliedto load Java String Variable");
returnNULL;
}
jStr=(*env)->GetObjectField(env,jobj,vFieldID);
if(jStr==NULL)
{
LOGI("Faliedto get jStr Object");
returnNULL;
}
vpTemp=(*env)->GetStringUTFChars(env,jStr,NULL);
if(vpTemp==NULL)
{
LOGI("Faliedto get UTF8 String Variable");
returnNULL;
}
LOGI("jStr:%s",vpTemp);
(*env)->ReleaseStringUTFChars(env,jStr,vpTemp);
jStr=(*env)->NewStringUTF(env,"JNIC Copy to Java String Variable");
if(jStr==NULL)
{
LOGI("Faliedto allocate UTF8 String Variable");
returnNULL;
}
(*env)->SetObjectField(env,jobj,vFieldID,jStr);
/
//Getand Set Double from Java
//
vFieldID=(*env)->GetFieldID(env,vClass,"DoubleInJava","D");
if(vFieldID==NULL)
{
LOGI("Faliedto load Java Double Variable");
returnNULL;
}
jDouble=(*env)->GetDoubleField(env,jobj,vFieldID);
if(jDouble==0)
{
LOGI("Faliedto get jDouble Object");
returnNULL;
}
LOGI("jDouble:%f",jDouble);
jDouble=9876.5432;
(*env)->SetDoubleField(env,jobj,vFieldID,jDouble);
/
jstringvStr=(*env)->NewStringUTF(env, gTest8Buffer);
returnvStr;
}
透過如下命令傳到手機上
adb push libtest8e.so /system/lib
334 KB/s (5136 bytes in 0.015s)
如下所示,為搭配上述JNI C程式碼參考到Java中變數,所配套的Java程式碼
package loda.Test8e;
import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
public class Test8e extends Activity {
private native String HelloWorld();
private int IntInJava;
private String StringInJava;
private double DoubleInJava;
static {
System.loadLibrary("test8e");
}
@Override
public void onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
IntInJava=99999;
StringInJava="String fromJava";
DoubleInJava=7777.6666;
Log.i("loda","libtest8e Before Call JNI HelloWorld");
String vRet= HelloWorld();
Log.i("loda:","IntInJava:" + IntInJava);
Log.i("loda:","StringInJava:" + StringInJava);
Log.i("loda:","DoubleInJava:" + DoubleInJava);
Log.i("Return String:",vRet);
Log.i("loda","libtest8e After Call JNI HelloWorld");
setContentView(R.layout.main);
}
}
接下來,讓我們示範由JNI C程式碼中,透過初始化要使用的Java Class與使用所包含的Method
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define LOG_TAG "C LOG DEMO"
#undef LOG
#include <utils/Log.h>
char gTest8Buffer[]="Sring fromJNI-C";
JNIEXPORT jstring JNICALLJava_loda_Test8f_Test8f_HelloWorld(JNIEnv * env, jobject jobj)
{
jmethodIDvMethodID;
jobjectvNewObj;
jclassvClass=(*env)->FindClass(env,"loda/Test8f/MyJavaClass");
if(vClass==NULL)
{
LOGI("Faliedto FindClass MyJavaClass");
returnNULL;
}
/
vMethodID=(*env)->GetMethodID(env,vClass,"<init>","()V");
if(vMethodID==NULL)
{
LOGI("Falied to Get init ofMyJavaClass");
returnNULL;
}
vNewObj=(*env)->NewObject(env,vClass,vMethodID, NULL);
/
vMethodID=(*env)->GetMethodID(env,vClass,"MyRun","(Ljava/lang/String;)Ljava/lang/String;");
if(vMethodID==NULL)
{
LOGI("Faliedto Get MyRun of MyJavaClass");
returnNULL;
}
LOGI("GetMyJavaClass Method MyRun");
jstringvStr=(*env)->NewStringUTF(env, gTest8Buffer);
jstringvRetStr=(*env)->CallObjectMethod(env,vNewObj,vMethodID,vStr);
constjbyte *jni_string=(*env)->GetStringUTFChars(env,vRetStr,0);
if(jni_string==NULL)
{
LOGI("Falied toGetStringUTFChars");
returnNULL;
}
LOGI("JavaMethod Return String:%s",jni_string);
(*env)->ReleaseStringUTFChars(env,vRetStr,jni_string);
(*env)->DeleteLocalRef(env,vClass);
returnvStr;
}
透過如下命令傳到手機上
adb push libtest8f.so /system/lib
417 KB/s (5132 bytes in 0.012s)
如下所示,為搭配上述JNI C程式碼參考到Java中變數,所配套的Java程式碼
package loda.Test8f;
import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
public class Test8f extends Activity {
privatenative String HelloWorld();
static{
System.loadLibrary("test8f");
}
@Override
publicvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String vRet= HelloWorld();
Log.i("Return String:", vRet);
setContentView(R.layout.main);
}
}
class MyJavaClass implements Runnable
{
publicvoid run()
{
}
publicString MyRun(String StringFromJNIC)
{
try
{
Log.i("loda:","in MyJavaClass:" + StringFromJNIC);
}
catch(Exceptione)
{
e.printStackTrace();
}
Log.i("loda:","Endof MyRun");
return"From MyJavaClass/MyRun in Java";
}
}
透過C程式要啟動Dalvik虛擬器,會透過呼叫函式JNI_CreateJavaVM (實作在dalvik/vm/Jni.c中),之後透過函式dvmStartup(實作在dalvik/vm/Init.c),再呼叫函式dvmPrepMainThread(實作在dalvik/vm/Thread.c)產生Dalvik虛擬器的Main Thread.
Google從2.2版Foryo開始釋出NDK r4的版本,支援JNI單步除錯的機制.除了可以直接透過JNI介面開發Java與Native Code的整合應用外,Google當然也提供了NDK的Framework,讓有這需求的開發者可以有一定程度的規範.
接下來,我們就針對JNI的介面進一步的追蹤系統運作的原理,
以hello-jni範例提供的JNI C函式Java_com_example_hellojni_HelloJni_stringFromJNI為例,進入到Java_com_example_hellojni_HelloJni_stringFromJNI函式後,此時的Call Stack為
(gdb) bt full
#0 Java_com_example_hellojni_HelloJni_stringFromJNI(env=0xaa50,thiz=0x43e1d078)
atC:/cygwin/android-ndk-r5b-windows/samples/hello-jni/jni/hello-jni.c:30
No locals.
#1 0x80213978 in ?? ()
確認Registers LR與PC
lr 0x80213978 2149661048
pc 0x803002e4 0x803002e4<Java_com_example_hellojni_HelloJni_stringFromJNI+8>
進一步確認上一層記憶體0x80213974中的程式邏輯
0x8021396c: ldmia r9, {r2, r3}
0x80213970: ldr r12, [r4, #8]
0x80213974: blx r12 =>由此跳到Java_com_example_hellojni_HelloJni_stringFromJNI中執行C函式庫
0x80213978: cmp r5, #0 ; 0x0
0x8021397c: ldrne r12, [r4, #12]
0x80213980: stmneia r12,{r0, r1}
0x80213984: ldmdb r4, {r4, r5, r6, r7, r8, r9, sp, pc}
0x80213988: mov r5, r2, lsr #28
0x8021398c: ldr r6,[r4, #4]
0x80213990: mov r2, #0 ; 0x0
0x80213994: ldrb r12, [r6], #1
0x80213998: cmp r12, #0 ; 0x0
0x8021399c: beq 0x802139c0
0x802139a0: add r2, r2, #1 ; 0x1
0x802139a4: cmp r12, #68 ; 0x44
0x802139a8: cmpne r12, #74 ; 0x4a
0x802139ac: bne 0x80213994
由上述的程式碼,我們可以對應到JNI呼叫到Native Code中的實作代碼是在dalvik/vm/arch/arm/CallEABI.S原始碼中的Lcopy_done程式碼,負責由Java World呼叫到JNI Native Code
.Lcopy_done:
...................
ldmia r9, {r2-r3} @ r2/r3<- argv[0]/argv[1]
@ call the method
ldr ip, [r4, #8] @ func
#ifdef __ARM_HAVE_BLX
blx ip
#else
mov lr, pc
bx ip
#endif
由此溯源,我們得知函式dvmPlatformInvoke是在Dalvik Vm中負責將Java World與Native World做一個呼叫轉換的函式.參考dalvik/vm/arch中的實作,Android本身有提供對arm/sh/x86/x86-atom JNI Calling Convention的組語優化,如果不是上述平台,就會透過FFI (Foreign Function Interface)(網址:http://sourceware.org/libffi/ )函式庫來實作函式dvmPlatformInvoke,達成JNI介面在不同平台上的支援. FFI機制,提供一個完整的介面,讓從Interpreter端可以呼叫到原生碼的實作,包括參數的傳遞,與對不同平台的支持,參考網頁,除了Dalvik JNI介面有採用外,包括FireFox 3.6/Mac OSX/iPhone JavaScript,Ruby,Python ..etc都有使用這個介面達成由Interpreter呼叫到原生碼的機制.
Just-In-TimeCompiler
有關JIT把ByteCode轉成原生指令集的概念,前兩天也聽到同事在聊LLVM(Low level virtual machine, 網址http://llvm.org/ ),這概念是把C/C++/Fortran的程式語言透過llvm-gcc轉成中介語言,之後透過LLVM轉換成執行平台原生的指令集,有關程式碼與中介語言的產生,可以透過該網站的Demo網頁http://llvm.org/demo/index.cgi實際的操刀體驗看看.
Dalvik系統除了有Portable與Fast Intepreter實作外,還可以透過JIT的機制,實現把Dalvik OpCode轉成原生代碼的機制,讓運作的效率可以變得更高,參考dvmMterpStdRun的實作,我們可以知道JIT在Dalvik中的入口是透過Fast Interpreter,如果你所在平台用的是Portable Interpreter,那JIT的機制也就沒有作用了.(ㄟ也是啦,,,,Fast Interpreter就是Dalvik用組語去刻出來的加速Dalvik OpCode直譯器,如果你不是用ARM或是x86的指令集平台,或其他第三方有支援Fast Interpreter組語實作的Dalvik版本,只能採用C版本的實作,自然也不可能會有JIT機制的實作了.).
可以參考如下的新聞Android 2.2 Froyo 450% Faster Than Eclair(網頁:http://www.mobiletopsoft.com/board/7646/android-2-2-froyo-450-faster-than-eclair.html),在有開啟JIT的Android 2.2 Froyo版本中,可以得到比沒有開啟JIT的2.1 Eclair大約4.5倍的Dalvik應用執行效率.(分別為37.5 MFLOPS 與 6.5-7 MFLOPS).
參考Google的文件,JIT的技術,有很多種施行的方式,包括在載入時期編譯,安裝應用程式時編譯,函式被呼叫時編譯整個函式,或是Byte Code指令在被擷取時編譯,而編譯時,可以選擇把整個應用程式編譯,動態函式庫,針對Class的Method編譯,Trace-JIT(選擇熱點編譯),或一次只編譯一個Byte Code指令.Android在選擇JIT技術時,主要希望以符合移動,透過電池供電,最小的記憶體需求(可以參考Jazelle網頁http://www.arm.com/products/processors/technologies/jazelle.php,JavaByte Code to Native Code記憶體會膨脹4-8倍),要跟既有的Dalvik安全機制共存,快速達到效能的提升,能在既有的直譯器(Intepreter)與JIT技術間平順的過渡.一般常見的Java虛擬器(例如SUN Java HotSpot Virtual Machine)採用的是Method-JIT,而Dalvik採用的是Trace-JIT,兩者的差別如下
Method JIT | 1,目前最常見的JIT機制 2,透過直譯器(Interpreter)去統計出熱門的Class Method 3,編譯與最佳化以Java Class Method為單位 4,有關虛擬器對應到直譯器的資訊,只需要考慮到所走過的Method為主. 5,缺點在於,所最佳化的Mehtod中,可能會包含幾乎用不到的程式碼片段,由於優化的範圍大,因此對記憶體需求較高,在函式一開始執行時,呼叫到熱門Mehtod,需要有比較長的延遲時間(等待最佳化為機械碼) |
Trace JIT | 1,透過直譯器去統計熱門的執行路徑(Execution Path) 2,會把編譯的程式碼片段串(Fragment Chain)放到轉譯的Cache中(Dalvik-jit-code-cache) 3,只有最熱門的程式碼執行路徑(片段)會被編譯到,可以節省記憶體的需求,並且可以跟直譯器更緊密的結合,擁有較快速的編譯與執行效能(因為不用根據整個Method去編譯) 4,每個Dalvik行程都有獨立的Dalvik虛擬器,並擁有一個Translation Cache(用來儲存編譯為機械碼的Trace段落). 5,缺點在於,優化的範圍較小,會跟直譯器互動的頻率高,並且,較難以把最佳化過的程式碼片段跨行程分享.(由於不是以Method為單位,只根據每個Mehtod中的程式碼片段優化,每個應用程式的行為差異不小,不過這有一篇交大碩士的論文http://www.cis.nctu.edu.tw/~wuuyang/papers/File-Based%20Sharing%20For%20Dynamically%20Compiled%20Code%20On%20Dalvik%20Virtual%20Machine.pdf ,有提到如何在Dalvik中跨行程分享這優化過的程式碼片段) 6,Android建議把JIT視為直譯器能力的延伸 7,確保Trace段落,只會包含有成功執行過的Byte Code 8,會根據使用的平台(通常為ARM),優化暫存器的使用,Load/Store指令的使用(減少需要去外部記憶體存取的成本),消除不必要的Null-Check. 9,優化Byte Code中Loop迴圈的執行. 10,通常會以一個Branch的目標位址作為一個Trace區塊的頭.(以此為Trace區塊也合理,便於在Translation Cache與Interpreter之間進行跳躍). |
參考這份Android文件http://www.android-app-developer.co.uk/android-app-development-docs/android-jit-compiler-androids-dalvik-vm.pdf,Android Dalvik基於記憶體需求,功耗(For移動裝置)與應用程式反應時間,選擇了Trace-JIT為實作的技術,運作的機制為,應用程式先透過Fast Interpreter載入,依據執行的位置,更新程式執行統計表格,,如果該位置沒有達到Threshold,直譯器繼續執行,直到到達下一個可能的Trace段落,再度確認統計表格,是否有達到Threshold,若達到就確認該Trace段落,是否已經有對應編譯的結果,如果有就直接跳過去執行,如果沒有就編譯該Trace段落,並加入到JIT轉譯快取(Translation Cache)中. 位於Translation中的Trace段落編譯結果,可以依據應用程式的行為彼此呼叫,如果該編譯段落的下一段Trace Fragment並不在Translation Cache中,就直接呼叫回直譯器執行該Trace Fragment,繼續Dalvik應用程式的執行.
ARMv5te架構下gDvmJit.threshold = 200 (in Sorce Codecompiler/codegen/arm/armv5te/ArchVariant.c),而ARMv7a架構下gDvmJit.threshold = 40 (compiler/codegen/arm/armv7-a/ArchVariant.c),以此來看,處理器的時脈越高,Android對於Trace區塊Threshold的要求就越低,也就是會盡可能都透過JIT編譯讓運作環境更快.(當然也表示對同一Android區塊,JIT的編譯時間越短).
參考Android文件(抱歉,,我就不自己OProfile了...@_@),Dalvik JIT對Trace段落熱區的統計,可以達到有97%是落在Translation Cache(Dalvik-jit-code-cache)中(在Cache中的Trace段落可以彼此呼叫,直到遇到不在Cache中的Trace段落才需要回到Interpreter),只有3%需要透過Fast Interpreter (Libdvm.so)執行.
承上,以system_server為例,在開機後20分鐘,大約有9898個Trace段落被編譯,總共佔Byte Code大小約103966bytes(平均每個Trace段落Byte Code約11bytes),編譯需要時間約6.024秒,編譯後的機械碼大小為796264 bytes (平均每個Trace段落轉成機械碼約80bytes),所涉及的Method Byte Code程式碼大小為396230 bytes(如果用Trace-JIT可以省去編譯2.8倍的ByteCode範圍),相對於省去的JIT機械碼大小就更可觀了(Method Byte Code程式碼大小:396230 bytes 乘上7倍(約80/11)=2773610bytes,約可省下編譯後約1.977364MB).
Dalvik在選擇JIT Trace區塊的時候,會在以下OpCode中進行Profile的統計,找出熱門的執行Trace區塊(參考dalvik/vm/mterp/out/InterpAsm-armv5te.S實作),作為Trace區塊的Head.
指令集名稱 | 編碼 |
OP_GOTO | 0x28 |
OP_GOTO_16 | 0x29 |
OP_GOTO_32 | 0x2a |
OP_PACKED_SWITCH | 0x2b |
OP_SPARSE_SWITCH | 0x2c |
OP_IF_EQ | 0x32 |
OP_IF_NE | 0x33 |
OP_IF_LT | 0x34 |
OP_IF_GE | 0x35 |
OP_IF_GT | 0x36 |
OP_IF_LE | 0x37 |
OP_IF_EQZ | 0x38 |
OP_IF_NEZ | 0x39 |
OP_IF_LTZ | 0x3a |
OP_IF_GEZ | 0x3b |
OP_IF_GTZ | 0x3c |
OP_IF_LEZ | 0x3d |
我們舉實際的例子來說明,不過首先定義一下,
在ARM EABI GCC編譯器中,對於ARM暫存器的識別名稱
暫存器 | 識別名稱 | 說明 |
r10 | sl | seems to be generally available |
r11 | fp | is used by gcc (unless -fomit-frame-pointer is set) |
r12 | ip | is scratch -- not preserved across method calls |
r13 | sp | should be managed carefully in case a signal arrives |
r14 | lr | must be preserved |
r15 | pc | can be tinkered with directly |
在Fast Interpreter中,對於ARM暫存器的使用與識別名稱
暫存器 | 識別名稱 | 說明 |
r4 | rPC | interpreted program counter, used for fetching instructions |
r5 | rFP | interpreted frame pointer, used for accessing locals and args |
r6 | rGLUE | MterpGlue pointer |
r7 | rINST | first 16-bit code unit of current instruction |
r8 | rIBASE | interpreted instruction base pointer, used for computed goto |
還有參考dalvik/vm/Globals.h中對於gDvmJit的宣告 “extern struct DvmJitGlobals gDvmJit;”,其中DvmJitGlobals定義了JIT運作時,所需的相關變數與狀態資訊.以及dalvik/vm/Thread.h中對於Thread的宣告,建議可以一併理解.
參考dalvik/vm/mterp/common/asm-constants.h中,關於組語中會用到的變數定義,同時參考dalvik/vm/mterp/Mterp.h中的宣告"typedef InterpState MterpGlue;”與在原始碼dalvik/vm/interp/InterpDefs.h中關於 “typedefstruct InterpState”的原型,我定義幾個在後續說明時,會用到的MterpGlue struct參數名稱與說明
ARM 組語中的名稱 | C程式碼中的變數名稱 | 說明 |
offGlue_pJitProfTable | pJitProfTable | Array of profile threshold counters |
offGlue_jitThreshold | jitThreshold | 為判斷該目標位置,是否已達可進行JIT編譯為原生碼的要求,以筆者手中的版本來說,armv5te-vfp/armv5te定義為200,而armv7-a-neon/armv7-a定義為40即達可進行JIT編譯的門檻. |
| pJitEntryTable | 會透過JIT Hash機制,用來儲存在Fast Interpret中有被編譯為原生碼的目標位置Trace段落. |
offThread_inJitCodeCache | inJitCodeCache | 相對於每個Dalvik Thread,用來儲存對應的Trace區塊編譯後的原生碼記憶體位址 |
offGlue_jitState | jitState | 設定Debug或要取Trace區塊時,Interpreter所在的狀態,可以包括以下的屬性 kJitNot = 0, // Non-JIT related reasons */ kJitTSelectRequest = 1, // Request a trace (subject to filtering) kJitTSelectRequestHot = 2, // Request a hot trace (bypass the filter) kJitSelfVerification = 3, // Self Verification Mode
/* Operational states in the debug interpreter */ kJitTSelect = 4, // Actively selecting a trace kJitTSelectEnd = 5, // Done with the trace - wrap it up kJitSingleStep = 6, // Single step interpretation kJitSingleStepEnd = 7, // Done with single step, ready return to mterp kJitDone = 8, // Ready to leave the debug interpreter |
| InterpEntry | 組語的宣告 MTERP_CONSTANT(kInterpEntryInstr, 0) MTERP_CONSTANT(kInterpEntryReturn, 1) MTERP_CONSTANT(kInterpEntryThrow, 2) MTERP_CONSTANT(kInterpEntryResume, 3) C的宣告 kInterpEntryInstr = 0, // continue to next instruction kInterpEntryReturn = 1, // jump to method return kInterpEntryThrow = 2, // jump to exception throw kInterpEntryResume = 3, // Resume after single-step |
offGlue_entryPoint | entryPoint | 用來儲存所要去的Interpreter |
接下來,進行流程上的說明,在OP_GOTO中,如果有加入JIT的支援就會,加入這段Code (我會把認為流程上可以忽略的Code “....”掉)
mov r0, rINST, lsl #16 @ r0<- AAxx0000
movs r9, r0, asr #24 @ r9<- ssssssAA (sign-extended)
mov r9, r9, lsl #1 @ r9<- byte offset=>r9儲存Goto要跳去的Offset值
=>如果r9為負數,表示會往低位址跳,會跳到函式common_backwardBranch中執行
bmi common_backwardBranch @ backward branch, do periodic checks
=>如果r9為正數,就繼續執行
#if defined(WITH_JIT)
GET_JIT_PROF_TABLE(r0)
FETCH_ADVANCE_INST_RB(r9) @ update rPC, load rINST
cmp r0,#0
bne common_updateProfile
GET_INST_OPCODE(ip) @ extract opcode from rINST
GOTO_OPCODE(ip) @ jump to next instruction
#else
…...
#endif
其中函式common_backwardBranch的實作如下,
common_backwardBranch:
mov r0, #kInterpEntryInstr
…...
#if defined(WITH_JIT)
GET_JIT_PROF_TABLE(r0)
FETCH_ADVANCE_INST_RB(r9) @ update rPC, load rINST
cmp r0,#0
bne common_updateProfile
GET_INST_OPCODE(ip)
GOTO_OPCODE(ip)
#else
…...
#endif
巨集FETCH_ADVANCE_INST_RB(宣告為#defineFETCH_ADVANCE_INST_RB(_reg) ldrh rINST, [rPC, _reg]!),會把帶入的GOTO目標偏移植(Offset)當做rPC(rPC為Fast Interpreter中的Program Counter)的偏移植,並把指令存在rINST,與更新下一個指令位置到rPC中.巨集GET_JIT_PROF_TABLE (宣告為#define GET_JIT_PROF_TABLE(_reg) ldr _reg,[rGLUE,#offGlue_pJitProfTable]),會傳回目前ProfileThreshold Counters表格的記憶體位置,如果該值為0,表示目前並沒有啟動指令Profiling Threshold的機制,就執行原本Fast Interpreter的流程,如果該值不為0,就表示JIT的Profiling Threshold機制有開啟,就跳到函式common_updateProfile進行處理.如下所示
common_updateProfile:
=>rPC儲存的是GOTO要去執行的目標位置
eor r3,rPC,rPC,lsr #12 @ cheap, but fast hashfunction
=>在筆者ARM_ARCH_5TE組態中,JIT_PROF_SIZE_LOG_2=9,其它平台為11
lsl r3,r3,#(32 - JIT_PROF_SIZE_LOG_2) @ shift out excess bits
=>以在Fast Interpreter中的ProgramCounter對Profile Threshold Counters表格(位置在r0)做Hash,然後把目前目標位置的Threshold值,放到r1
ldrb r1,[r0,r3,lsr #(32 - JIT_PROF_SIZE_LOG_2)]@ get counter
=>把rINST & 0xff存到r12中,成為一個Dalvik OpCode
GET_INST_OPCODE(ip)
subs r1,r1,#1 @ decrement counter
strb r1,[r0,r3,lsr #(32 - JIT_PROF_SIZE_LOG_2)]@ and store it
=>如果r1不為0,就直接跳到該Dalvik OpCode所在的位址,表示該目標位置尚未達Threshold的要求,還是執行Fast Interpreter的原本機制,不編譯為原生碼.
GOTO_OPCODE_IFNE(ip) @ if not threshold, fallthroughotherwise */ (CPSR.Z=0)
=>如果r1為0,就繼續執行,根據Trace區塊,進行編譯的流程.
…..........................
=>取得目前平台預設的Threshold數值
GET_JIT_THRESHOLD(r1)
ldr r10, [rGLUE,#offGlue_self] @ callee saved r10 <- glue->self
=>重設該目標位置的Threshold Counter,ARMv5預設是200,ARMv7預設是40
strb r1,[r0,r3,lsr #(32 -JIT_PROF_SIZE_LOG_2)] @ reset counter
=>把Fast Interpreter要去執行的目標位置,存到Interpreted Frame(Stack)中
EXPORT_PC()
mov r0,rPC
bl dvmJitGetCodeAddr @ r0<- dvmJitGetCodeAddr(rPC)
=>呼叫函式dvmJitGetCodeAddr ,如果位於FastInterpreter的Program Counter已經有被編譯過,並儲存在Translation Cache中,就傳回對應的記憶體位置
str r0, [r10,#offThread_inJitCodeCache] @ set the inJitCodeCache flag
=>把編譯過的Code Cache記憶體位址儲存在#offGlue_self中的#offThread_inJitCodeCache欄位中
mov r1, rPC @ arg1 of translation mayneed this
mov lr, #0 @ in case target is HANDLER_INTERPRET
cmp r0,#0
=>如果r0為0,表示尚未編譯過,反之表示編譯過,存在Translation Cache中
#if !defined(WITH_SELF_VERIFICATION)
...............
#else
=>如果目前Fast Interpreter要去的ProgramCounter尚未編譯過,就設定r2為selectTrace Request然後呼叫函式common_selectTrace
moveq r2,#kJitTSelectRequest @ askfor trace selection
beq common_selectTrace
/*
* At this point, we have a target translation. However, if
* that translation is actually the interpret-only pseudo-translation
* we want to treat it the same as no translation.
*/
mov r10, r0 @ save target
bl dvmCompilerGetInterpretTemplate
cmp r0, r10 @ special case?
bne jitSVShadowRunStart @ set up self verification shadowspace
GET_INST_OPCODE(ip)
GOTO_OPCODE(ip)
/* no return */
#endif
巨集GET_JIT_THRESHOLD (宣告為#define GET_JIT_THRESHOLD(_reg) ldr _reg,[rGLUE,#offGlue_jitThreshold]),巨集EXPORT_PC(宣告為#define EXPORT_PC() str rPC, [rFP, #(-sizeofStackSaveArea + offStackSaveArea_currentPc)]),由函式common_selectTrace會先回到函式dvmMterpStd中(為標準的mterp進入點,實作位置在dalvik/vm/mterp/Mterp.c),並設定glue->nextMode= INTERP_DBG與傳回true,之後回到函式dvmInterpret(實作的位置在dalvik/vm/interp/Interp.c),因為選擇了Debug版本的Interpreter,呼叫進入dvmInterpretDbg(定義為#define INTERP_FUNC_NAME dvmInterpretDbg ),在函式INTERP_FUNC_NAME(實作在dalvik/vm/mterp/out/InterpC-portdbg.c),再呼叫函式dvmJitCheckTraceRequest(實作在dalvik/vm/interp/Jit.c),再透過函式setTraceConstruction進行Trace的建置,最後呼叫函式dvmCheckJit(實作在dalvik/vm/interp/Jit.c)會一次Interpreter切換週期(由Fast Interpreter切到Debug Interpreter),增加一個指令集的方式進行Trace流程的追蹤,目前一個Trace流程最多可以有64個TraceRun(MAX_JIT_RUN_LEN=64),也就是說如果你是在函式內GOTO到不同位置的話,這些路徑就會被當做個別的TraceRun,最後,放到同一個Trace流程中.如果是在有Switch操作的函式中,TraceRun的選擇會以一個Switch內的處理指令流程為主,如果離開所在的Switch判斷,就會作為一個Trace流程的結束.在dvmCheckJit內部會有以下的判斷依據
1,如果目前已執行Trace指令個數長度不為0,且上一次處理的指令為OP_PACKED_SWITCH或OP_SPARSE_SWITCH,就會將Trace流程結束.
2,如果上一個指令不是GOTO,也不為INVOKE_DIRECT_EMPTY ,而屬於像是IF/Switch/Return/Invoke類的指令,也會作為一個Trace流程的結束.
3,如果上一個指令是一個Exception或是一個Self-Loop,也會作為一個Trace Run的結束.
4,如果Trace的指令個數超過100個(JIT_MAX_TRACE_LEN=100),也會當做一個Trace流程的結束.
5,如果前一個指令為Return,也會當做一個Trace流程的結束.
當上述Trace流程結束就會把interpState->jitState設定為kJitTSelectEnd,或是直接進入到switch kJitTSelectEnd中,結束Trace流程.
在函式dvmCheckJit中會判斷,如果是第一個Trace區塊的指令,會把當前的Program Counter記錄在interpState->lastPC,然後,直接結束.第二次進來時,會呼叫dexDecodeInstruction(實作在dalvik/libdex/InstrUtils.c)把上一次的ProgramCounter進行指令集的Decode,如果解碼後的指令為OP_PACKED_SWITCH或是OP_SPARSE_SWITCH,就會設定interpState->jitState = kJitTSelectEnd把Trace區塊做一個結束.若繼續執行,就會透過函式dexGetInstrFlags(Inline函式宣告在libdex/InstrUtils.h中),並依據指令集的OpCode決定Flags為以下的值之一
Dalvik 指令 | 對應到的Flags狀態 |
OP_NOP,OP_MOVE,OP_MOVE_FROM16,OP_MOVE_16,OP_MOVE_WIDE, OP_MOVE_WIDE_FROM16,OP_MOVE_WIDE_16,OP_MOVE_OBJECT, OP_MOVE_OBJECT_FROM16,OP_MOVE_OBJECT_16,OP_MOVE_RESULT, OP_MOVE_RESULT_WIDE,OP_MOVE_RESULT_OBJECT, OP_MOVE_EXCEPTION,OP_CONST_4,OP_CONST_16,OP_CONST, OP_CONST_HIGH16,OP_CONST_WIDE_16,OP_CONST_WIDE_32, OP_CONST_WIDE,OP_CONST_WIDE_HIGH16,OP_FILL_ARRAY_DATA, OP_CMPL_FLOAT,OP_CMPG_FLOAT,OP_CMPL_DOUBLE, OP_CMPG_DOUBLE,OP_CMP_LONG,OP_NEG_INT,OP_NOT_INT, OP_NEG_LONG,OP_NOT_LONG,OP_NEG_FLOAT,OP_NEG_DOUBLE, OP_INT_TO_LONG,OP_INT_TO_FLOAT,OP_INT_TO_DOUBLE, OP_LONG_TO_INT,OP_LONG_TO_FLOAT,OP_LONG_TO_DOUBLE, OP_FLOAT_TO_INT,OP_FLOAT_TO_LONG,OP_FLOAT_TO_DOUBLE, OP_DOUBLE_TO_INT,OP_DOUBLE_TO_LONG,OP_DOUBLE_TO_FLOAT, OP_INT_TO_BYTE,OP_INT_TO_CHAR,OP_INT_TO_SHORT,OP_ADD_INT, OP_SUB_INT,OP_MUL_INT,OP_AND_INT,OP_OR_INT,OP_XOR_INT, OP_SHL_INT,OP_SHR_INT,OP_USHR_INT,OP_ADD_LONG,OP_SUB_LONG, OP_MUL_LONG,OP_AND_LONG,OP_OR_LONG,OP_XOR_LONG, OP_SHL_LONG,OP_SHR_LONG,OP_USHR_LONG,OP_ADD_FLOAT, OP_SUB_FLOAT,OP_MUL_FLOAT,OP_DIV_FLOAT,OP_REM_FLOAT ,OP_ADD_DOUBLE,OP_SUB_DOUBLE,OP_MUL_DOUBLE,OP_DIV_DOUBLE, OP_REM_DOUBLE,OP_ADD_INT_2ADDR,OP_SUB_INT_2ADDR, OP_MUL_INT_2ADDR,OP_AND_INT_2ADDR,OP_OR_INT_2ADDR, OP_XOR_INT_2ADDR,OP_SHL_INT_2ADDR,OP_SHR_INT_2ADDR, OP_USHR_INT_2ADDR,OP_ADD_LONG_2ADDR,OP_SUB_LONG_2ADDR, OP_MUL_LONG_2ADDR,OP_AND_LONG_2ADDR,OP_OR_LONG_2ADDR, OP_XOR_LONG_2ADDR,OP_SHL_LONG_2ADDR,OP_SHR_LONG_2ADDR, OP_USHR_LONG_2ADDR,OP_ADD_FLOAT_2ADDR,OP_SUB_FLOAT_2ADDR, OP_MUL_FLOAT_2ADDR,OP_DIV_FLOAT_2ADDR,OP_REM_FLOAT_2ADDR, OP_ADD_DOUBLE_2ADDR,OP_SUB_DOUBLE_2ADDR, OP_MUL_DOUBLE_2ADDR,OP_DIV_DOUBLE_2ADDR, OP_REM_DOUBLE_2ADDR,OP_ADD_INT_LIT16,OP_RSUB_INT, OP_MUL_INT_LIT16,OP_AND_INT_LIT16,OP_OR_INT_LIT16, OP_XOR_INT_LIT16,OP_ADD_INT_LIT8,OP_RSUB_INT_LIT8, OP_MUL_INT_LIT8,OP_AND_INT_LIT8,OP_OR_INT_LIT8,OP_XOR_INT_LIT8, OP_SHL_INT_LIT8,OP_SHR_INT_LIT8,OP_USHR_INT_LIT8 | flags = kInstrCanContinue; 表示這類指令會加入Trace區塊,並不會造成Exception |
OP_CONST_STRING,OP_CONST_STRING_JUMBO,OP_CONST_CLASS, OP_MONITOR_ENTER,OP_MONITOR_EXIT,OP_CHECK_CAST, OP_INSTANCE_OF,OP_ARRAY_LENGTH,OP_NEW_INSTANCE, OP_NEW_ARRAY,OP_FILLED_NEW_ARRAY,OP_FILLED_NEW_ARRAY_RANGE ,OP_AGET,OP_AGET_BOOLEAN,OP_AGET_BYTE,OP_AGET_CHAR, OP_AGET_SHORT,OP_AGET_WIDE,OP_AGET_OBJECT,OP_APUT, OP_APUT_BOOLEAN,OP_APUT_BYTE,OP_APUT_CHAR,OP_APUT_SHORT, OP_APUT_WIDE,OP_APUT_OBJECT,OP_IGET,OP_IGET_BOOLEAN, OP_IGET_BYTE,OP_IGET_CHAR,OP_IGET_SHORT,OP_IGET_WIDE, OP_IGET_OBJECT,OP_IPUT,OP_IPUT_BOOLEAN,OP_IPUT_BYTE, OP_IPUT_CHAR,OP_IPUT_SHORT,OP_IPUT_WIDE,OP_IPUT_OBJECT, OP_SGET,OP_SGET_BOOLEAN,OP_SGET_BYTE,OP_SGET_CHAR, OP_SGET_SHORT,OP_SGET_WIDE,OP_SGET_OBJECT,OP_SPUT, OP_SPUT_BOOLEAN,OP_SPUT_BYTE,OP_SPUT_CHAR,OP_SPUT_SHORT, OP_SPUT_WIDE,OP_SPUT_OBJECT,OP_DIV_INT,OP_REM_INT, OP_DIV_LONG,OP_REM_LONG,OP_DIV_INT_2ADDR,OP_REM_INT_2ADDR, OP_DIV_LONG_2ADDR,OP_REM_LONG_2ADDR,OP_DIV_INT_LIT16, OP_REM_INT_LIT16,OP_DIV_INT_LIT8,OP_REM_INT_LIT8, OP_EXECUTE_INLINE,OP_EXECUTE_INLINE_RANGE,OP_IGET_QUICK, OP_IGET_WIDE_QUICK,OP_IGET_OBJECT_QUICK,OP_IPUT_QUICK, OP_IPUT_WIDE_QUICK,OP_IPUT_OBJECT_QUICK | flags = kInstrCanContinue | kInstrCanThrow; 表示這類指令會加入Trace區塊,但有可能造成Exception
|
OP_INVOKE_VIRTUAL,OP_INVOKE_VIRTUAL_RANGE,OP_INVOKE_SUPER, OP_INVOKE_SUPER_RANGE,OP_INVOKE_DIRECT, OP_INVOKE_DIRECT_RANGE,OP_INVOKE_STATIC, OP_INVOKE_STATIC_RANGE,OP_INVOKE_INTERFACE, OP_INVOKE_INTERFACE_RANGE,OP_INVOKE_VIRTUAL_QUICK, OP_INVOKE_VIRTUAL_QUICK_RANGE,OP_INVOKE_SUPER_QUICK, OP_INVOKE_SUPER_QUICK_RANGE,OP_INVOKE_DIRECT_EMPTY | flags = kInstrCanContinue | kInstrCanThrow | kInstrInvoke; 表示這類指令會加入Trace區塊,有機會發生Exception或是進行外部呼叫 |
OP_RETURN_VOID,OP_RETURN,OP_RETURN_WIDE,OP_RETURN_OBJECT | flags = kInstrCanReturn; 表示為Return指令 |
OP_THROW,OP_THROW_VERIFICATION_ERROR | flags = kInstrCanThrow; 會觸發Exception |
OP_GOTO,OP_GOTO_16,OP_GOTO_32 | flags = kInstrCanBranch | kInstrUnconditional; 表示一個無條件的Branch動作 |
OP_IF_EQ,OP_IF_NE,OP_IF_LT,OP_IF_GE,OP_IF_GT,OP_IF_LE,OP_IF_EQZ, OP_IF_NEZ,OP_IF_LTZ,OP_IF_GEZ,OP_IF_GTZ,OP_IF_LEZ | flags = kInstrCanBranch | kInstrCanContinue; 表示一個搭配條件判斷的Branch動作 |
OP_PACKED_SWITCH,OP_SPARSE_SWITCH | flags = kInstrCanSwitch | kInstrCanContinue; 表示一個Switch判斷,如果該值不在這Switch中,會加入Trace區塊 |
OP_UNUSED_3E,OP_UNUSED_3F,OP_UNUSED_40,OP_UNUSED_41, OP_UNUSED_42,OP_UNUSED_43,OP_UNUSED_73,OP_UNUSED_79, OP_UNUSED_7A,OP_UNUSED_E3,OP_UNUSED_E4,OP_UNUSED_E5, OP_UNUSED_E6,OP_UNUSED_E7,OP_UNUSED_E8,OP_UNUSED_E9, OP_UNUSED_EA,OP_UNUSED_EB,OP_BREAKPOINT,OP_UNUSED_F1, OP_UNUSED_FC,OP_UNUSED_FD,OP_UNUSED_FE,OP_UNUSED_FF | these should never appear when scanning code flags=0 |
後續,呼叫dexGetInstrOrTableWidthAbs(實作在libdex/InstrUtils.c)判斷所在位置指令或是資料(PackedSwitchSignature/SparseSwitchSignature/ArrayDataSignature)的長度(指令長度的Table可以透過函式dexCreateInstrWidthTable建立),以及距離所在Class Method起始點的位置(lastPC – interpState->method->insns),如果超過該ClassMethod的實際實作的大小,就會觸發Assertion.
等到完成一個Trace流程段落,會呼叫函式dvmCompilerWorkEnqueue,把這個Trace流程段落放到JIT編譯器的Queue中,之後就會由Compiler的thread透過函式dvmCompileTrace(實作在dalvik/vm/compiler/Frontend.c)進行編譯為原生碼的動作.並重新回到函式dvmMterpStd,繼續下一個指令集的抓取與執行,直到又有指令Threshold被滿足,而進行下一次的Trace追蹤的流程.在進行Trace追蹤的流程時,相關指令都還是會以Fast Interpreter的實作被執行.
要在Android環境中支援JIT機制,必須在Dalvik編譯時加入WITH_JIT參數,可以參考dalvik/vm/Android.mk,如果所選擇的架構是armv5te (TARGET_ARCH_VARIANT:=armv5te),WITH_JIT預設會是關掉的.然後修改build/target/board/generic/system.prop加入dalvik.vm.execution-mode=int:jit(這個系統參數設定會被加入到執行時期的檔案/system/build.prop), ,就可以開始編譯支援Dalvik JIT的虛擬器.
支援JIT的Dalvik環境除了會編譯出libdvm.so外,參考dalvik/vm/Android.mk,在編譯Dalvik函式庫時,還會額外產生以下三個函式庫
libdvm_sv.so(開啟Assertion與self-verification)
libdvm_assert.so(開啟Assertion與JIT Tuning)
libdvm_interp.so(這是把WITH_JIT關閉的版本)
可根據自己開發上的除錯,決定要採用的版本.
要確認所編譯的Dalvik虛擬器有無啟動JIT機制,可以在啟動後,執行dalvikvm -h 查看是否有下列參數設定
-Xjitop:hexopvalue[-endvalue][,hexopvalue[-endvalue]]*
-Xincludeselectedmethod
-Xjitthreshold:decimalvalue
-Xjitblocking
-Xjitmethod:signature[,signature]*(eg Ljava/lang/String/;replace)
-Xjitcheckcg
-Xjitverbose
-Xjitprofile
-Xjitdisableopt
簡述ADB,Emulator與NDK-GDB運作的機制
在非直接使用實體手機裝置透過USB開發的環境下,我們可以透過電腦上的模擬器達到便利開發的目的,根據上述的流程,我們可以看到包括abd或是ndk-gdb這些輔助開發,安裝與除錯的工具,都可以跟Emulator互通.目前ADB,Emulator或是NDK-GDB都是透過網路TCP連線的方式進行互通.如果使用者使用Eclipse加上ADT的環境,一開始會產生一個ADB的Daemon,監聽port 5037,而Eclipse上的ADT套件,會產生連線到ADT的port 5037. 如果有執行Emulator的話,就會由ADT連到Emulator所監聽的Port5555,與Eclipse連到Emulator所監聽的Port 5554. Emulator的Port訊息也會透過ADB Port 5037交換,如下所示
Receive Side Port:5037
==>Return Code: 0x00000000
001Chost:transport:emulator-5554
Send Size Port:40280
==> Return Code: 0x00000000
OKAY
如果使用者透過ndk-gdb - -start啟動對JNI應用除錯機制的話,就會由arm-linux-androideabi-gdb.連到adb所監聽的Port 5039進行GDB的操作動作.
(Receive Port:5037 Send Port:49288)
Receive: Return Code: 0x00000000
0052host:forward:tcp:5039;localfilesystem:/data/data/com.example.hellojni/debug-socket
Send: Return Code: 0x00000000
OKAYOKAY
(Receive Port:5037 Send Port:49290)
Receive: Return Code: 0x00000000
0012host:transport-any
Send: Return Code: 0x00000000
OKAY
Receive: Return Code: 0x00000000
004ashell:run-as com.example.hellojnilib/gdbserver +debug-socket --attach 348
Send: Return Code: 0x00000000
OKAY
Send: Return Code: 0x00000000
Attached; pid = 348
Send: Return Code: 0x00000000
Listening on sockaddr socket debug-socket
(Receive Port:5039 Send Port:49295)
Receive: Return Code: 0x00000000
$qSupported#37
Send: Return Code: 0x00000000
+$PacketSize=7cf;qXfer:auxv:read+#70
Receive: Return Code: 0x00000000
++$Hc-1#09
Send: Return Code: 0x00000000
+$E01#a6
Receive: Return Code: 0x00000000
+$qC#b4
Send: Return Code: 0x00000000
+$#00
Receive: Return Code: 0x00000000
+$qOffsets#4b
Send: Return Code: 0x00000000
+$#00
Receive: Return Code: 0x00000000
+$?#3f
Send: Return Code: 0x00000000
+$T050b:0*"00;0d:9838a0be;0f:08ebd0af;#6e
Receive: Return Code: 0x00000000
+$m9040,108#ff
Send: Return Code: 0x00000000
+$010*"c4ca00b0010*"24cf00b0010*"3cd000b0010*"6cd200b0010*"84d300b0010*"dccb00b0010*"f4cc00b0010*"0cce00b020*%90*!210*"080*"190*"0890*!1b0*"080*"1a0*"1090*!1c0*"080*"040*"08810*!50*"ec840*!60*"4c820*!a0*"e5030*!b0*"10*"0150*"a8c400b0030*"48910*!20*"b0*"0140*"110*"170*"fc880*110*"d4880* 120*"280*"130*"080*}0*!#a8
Receive: Return Code: 0x00000000
+$mb000c4ac,4#1a
Send: Return Code: 0x00000000
+$b0ca00b0#48
Receive: Return Code: 0x00000000
+$mb000cab0,14#46
Send: Return Code: 0x00000000
+$0*"00db3da0be0*"00e04901b0*%#b1
Receive: Return Code: 0x00000000
+$mb00149e0,14#f3
Send: Return Code: 0x00000000
+$0*"00dc4801b0*%e0cc00b0b0ca00b0#e6
結語
本文謹涉及Trace JIT,JNI與有關Dalvik基礎的知識,後續,會視時間從系統運作的角度,來分析Dalvik在效能加速上的議題.
雖盡可能確保資訊的正確性,然若仍有所遺漏,還請不吝告知.感謝!!