dalvik的目標平台是Android這樣的小RAM,低速度flash memory,運行標准Linux系統的設備。針對這樣的平台特性,要想做到更好,我們需要考慮以下幾點:
1、為了減少系統的內存使用,字節碼可以多進程共享。但出於安全性考慮,這樣的字節碼不可以編輯。
2、為了保證響應速度,加載一個新的APP所需時間盡量少。
3、標准Java中把多個類文件分別存放導致了大量的冗余,為了節省APP的占用空間,這個問題要解決。
4、加載類的時候解析類的字段成員會導致額外的消耗,如果改成像C一樣直接訪問會比較好。
5、字節碼verification很有必要,但很慢,我們需要把驗證與APP執行分開。
6、字節碼optimization(比如指令優化、方法pruning)可以在很大程度上影響執行速度和電池消耗。
標准VM都是程序啟動時把每單個的類文件解壓放入heap,每個進程都有一份copy。這樣的做法在內存占用和時間上面都有損失,但方便了對指令的優化。
現在看看dalvik是怎麼做的:
1、多個類被集成進單一的DEX文件。
2、DEX文件在進程間以只讀方式共享。
3、byte ordering和word alignment根據local system來做調整。
4、字節碼verification盡可能提前。
5、需要修改字節碼的optimization必須提前進行。
這樣做的好處在下面一一介紹。
VM Operation
系統中的應用程序代碼以.jar或.apk文件存在。其實它們都是.zip的文檔,只不過多了一些文件頭信息。DEX文件也就是解壓.apk後的classes.dex文件。classes.dex中的字節碼是經過壓縮處理的,而且文件頭部不一定是word aligned,所以不能直接mmap到內存直接執行,而是先解壓,然後做一些realignment,optimization,verification操作。下面詳細介紹一下這個過程。
Preparation
做到DEX文件的執行前優化(優化後的DEX叫做ODEX,Optimization DEX),至少有三種方式:
1、VM的JIT技術。優化後的文件放在/data/dalvik-cache目錄下。這種方式在模擬器和eng模式下編譯的系統中有效,只有這兩種情況下操作dalvik-cache目錄才不會有權限問題。
2、安裝應用程序時,system installer做優化。這需要dalvik-cache目錄的寫權限。
3、編譯系統源碼時進行優化。這樣優化不會修改jar/apk文件,但會對classes.dex進行優化,優化後的DEX與原文件放在同一個目錄下一起寫入system image。
系統中的/data/dalvik-cache目錄屬於system/system,權限是0771。存儲在這個目錄下的ODEX文件被system和應用程序所屬的group擁有,權限是0644。DRM-locked的應用程序使用640權限。底線是你可以讀你自己的DEX文件和其它的大多數應用程序,但不能創建、修改或刪除它們。
使用JIT和system installer做DEX文件的Preparation要分成三步:
1、由system installer創建dalvik-cache文件夾,這個程序運行在有root權限的installd進程中。
2、classes.dex被解壓出來,並在文件頭部預留一些空間存放ODEX頭信息。
3、為了方便使用和做一些針對特定系統的微調,把它mmap。比如byte-swapping,structure realigning等。我們還會做一些像文件偏移量和數據索引是否越界等方面的基本檢查。
編譯系統使用一個很復雜的流程來做這些事:啟動模擬器,強制對所有相關DEX文件執行JIT優化,最後把優化後的結果從dalvik-cache中提取出來。之所以這樣做而不是在PC上面使用一個工具來完成,在後面解釋Optimization時可以看到原因。
當代碼的byte-swapping和align完成時,我們的preparation就完成了。再做完verification和optimization,最後,我們就會把一些相關計算出來的信息添加到ODEX文件的頭部然後開始執行。
dexopt
其實,如果我們想優化DEX中的類文件的話,最簡單最安全的辦法就是把所有類加載到VM中然後運行一遍,運行失敗的就是沒有verification和optimization的。但是,這樣會分配一些很難釋放的資源。比如,加載本地庫時。所以,不能使用運行程序的那個VM來做。
我們的解決方案就是使用dexopt這個程序,它會初始化一個VM,加載DEX文件並執行verification和optimization過程。完成後,進程退出,釋放所有資源。這個過程中,也可以多個VM使用同一個DEX。file lock會讓dexopt只運行一次。
verification
字節碼的verification過程涉及到每個DEX文件中的所有類和類中的所有方法中的指令。目標就是檢查非法指令序列,這樣做完以後,運行的時候就不必管了。這個過程中涉及到的許多計算也存在於GC過程中。
出於效率上的考慮,下一節提到的optimization會假設verification已經成功運行通過。默認情況下,dalvik會對所有類進行verification,而只對verification成功的類執行optimization。在進行verification過程中出現失敗時,我們不一定會報告(比如在不同的包中調用一個作用范圍為包內的類),我們會在執行時拋出一個異常。因為檢查每個方法的訪問權限很慢。
執行verification成功的類在ODEX文件中有一個flag set,當它們被加載時,就不會再進行verification。linux系統的安全機制會防止這個文件被破壞,但如果你能繞過去,還是能去破壞它的。ODEX文件有一個32-bit的checksum,但只能做一個快速檢查。
Optimization
VM解釋器在第一次運行一段代碼時會做一些optimization。比如,把常量池引用替換成指向內部數據結構的指針,一些永遠成功的操作或固定的代碼被替換成更簡單的形式。做這些optimization需要的信息有的只能在運行時得到,有的可以推斷出來。
dalvik做的optimization包含下面這些:
1、對於虛方法的調用,把方法索引修改成vtable索引。
2、把field的get/put修改成字節偏移量。把boolean/byte/char/short等類型的變量合並到一個32-bit的形式,更少的代碼可以更有效地利用CPU的I-cache。
3、把一些大量使用的簡單方法進行inline,比如String.length()。這樣能減少方法調用的開銷。
4、刪除空方法。
5、加入一些計算好的數據。比如,VM需要一個hash table來查找類名字,我們就可以在Optimization階段進行計算,不用放到DEX加載的時候了。
所有的指令修改都是使用一個Dalvik標准沒有定義的指令去替換原有指令。這樣,我們就可以讓優化和沒有優化的指令自由搭配。具體的操作與VM版本有關。
Optimization過程有兩個地方需要我們注意:
1、VM如果更新的話,vtable索引和字節偏移量可能會更新。
2、如果兩個DEX互相信賴,而其中一個DEX更新的話,確保優化後的索引和偏移量有效。
Dependencies and Limitations
優化後的DEX會包含一個它信賴的DEX文件列表,並添加了CRC-32和修改時間。文件列表中包含了dalvik-cache目錄下的文件的路徑和相應的SHA-1簽名。而文件在設備上的timestamp不可信也不能用。另外還有VM版本號。
如果當前DEX所依賴的DEX有更新,我們也需要更新當前DEX。如果我們可以做一個JIT的dexopt調用,更新過程很easy。但如果只能依賴installer daemon,或者這個DEX被裝到ODEX中的話,VM只能拒絕它了。
dexopt的輸出與平台版本,VM版本有關,想編寫一個運行在PC上,而優化後的輸出在其它設備使用的dexopt很難。因此,dexopt是在目標設備上或者目標設備的模擬器上運行。