1.Android預優化的原理
先來回顧一下Android的發展史,在2014年的Google I/O大會上,Google隆重的發布了Android 4.4操作系統,其中有一個環節着重介紹了ART(Android runtime),也就是虛擬機,也就是運行APP的環境,也就是運行Java代碼的虛擬機。雖然Android 4.4版本默認仍然用DVM(Dalvik VM),但到Android 5.0時ART就變成了默認模式,自此,DVM時代一去不再復返。
DVM和ART和Android的預優化有什么關系?先了解Java的幾種虛擬機基本的工作機制:
(1)JVM:JVM虛擬機運行的是java字節碼。Java文件到JVM的過程是:java -> java bytecode(class) -> java bytecode(jar)。
(2)DVM:DVM虛擬機解析執行的dex字節碼。Java文件到DVM的過程是:java -> java bytecode(class) -> dalvik bytecode(dex)。
(3)ART:ART虛擬機執行本地機器碼。Java文件到ART的過程是:java -> java bytecode(class) -> dalvik bytecode(dex) -> optimized android runtime machine code(oat)。
在Android 4.4(包括)之前用的是DVM,Android 4.4之后用的是ART,從java文件到虛擬機執行代碼,ART比DVM多了oat的過程,ART所使用的AOT(Ahead-Of-Time)編譯,在應用首次安裝時,字節碼預編譯成機器碼存在本地,而DVM是典型的JIT(Just-In-Time),在此模式下,應用每次運行的時候,字節碼都需要即時編譯器轉換為機器碼再執行,ART模式相對於DVM省去了解析字節碼的過程,占用內存也相應減少,進而提高APP的運行效率,所以Andorid 5.0發布時,Google號稱Android系統可以跑在512 M的內存的機器上,ART發揮了舉足輕重的作用。
2. ART 的運作方式
ART 使用預先 (AOT) 編譯,並且從 Android 7.0(代號 Nougat,簡稱 N)開始結合使用 AOT、即時 (JIT) 編譯和配置文件引導型編譯。所有這些編譯模式的組合均可配置,我們將在本部分中對此進行介紹。例如,Pixel 設備配置了以下編譯流程:
最初安裝應用時不進行任何 AOT 編譯。應用前幾次運行時,系統會對其進行解譯,並對經常執行的方法進行 JIT 編譯。
當設備閑置和充電時,編譯守護進程會運行,以便根據在應用前幾次運行期間生成的配置文件對常用代碼進行 AOT 編譯。
下一次重新啟動應用時將會使用配置文件引導型代碼,並避免在運行時對已經過編譯的方法進行 JIT 編譯。在應用后續運行期間進行了 JIT 編譯的方法將會被添加到配置文件中,然后編譯守護進程將會對這些方法進行 AOT 編譯。
ART 包括一個編譯器(dex2oat 工具)和一個為啟動 Zygote 而加載的運行時 (libart.so)。dex2oat 工具接受一個 APK 文件,並生成一個或多個編譯工件文件,然后運行時將會加載這些文件。文件的個數、擴展名和名稱會因版本而異,但在 Android O 版本中,將會生成以下文件:
.vdex:其中包含 APK 的未壓縮 DEX 代碼,另外還有一些旨在加快驗證速度的元數據。
.odex:其中包含 APK 中已經過 AOT 編譯的方法代碼。
.art (optional):其中包含 APK 中列出的某些字符串和類的 ART 內部表示,用於加快應用啟動速度。
詳細參考:https://source.android.google.cn/devices/tech/dalvik/configure?hl=zh-cn
3.Android 優化的過程
Zygote進程在啟動的過程中,創建ART虛擬機。在ART中,打包在APK里面的Dex字節碼是通過LLVM翻譯成本地機器指令的。LLVM是一個用來快速開發自己的編譯器的框架系統,在Dalvik運行時中,APK在安裝的時候,安裝服務PackageManagerService會通過守護進程installd調用一個工具 dexopt對打包在APK里面包含有Dex字節碼的classes.dex進行優化,優化得到的文件保存在/data/dalvik-cache目錄 中,並且以.odex為后綴名,表示這是一個優化過的Dex文件。在ART運行時中,APK在安裝的時候,同樣安裝服務 PackageManager Service會通過守護進程installd調用另外一個工具dex2oat對打包在APK里面包含有Dex字節碼進翻譯。翻譯后得到的是一個ELF格式的oat文件,這個oat文件同樣是以.odex后綴結束,並且也是保存在/data/dalvik-cache目錄中。
Android在首次啟動和首次安裝應用時,需要將字節碼翻譯成機器碼,這樣Android系統的啟動速度將會大大減慢,如果沒有預優化,APP的運行速度也會加上翻譯所需要的時間。所以,這個翻譯的工作需要轉移到編譯上面來,也就是所,在編譯APK文件時,將會預先對APK進行翻譯的優化,然后再打包到系統里面去,這樣Android系統在首次啟動時,就不再需要花費大量的時間去翻譯APK的字節碼。
4. Mstar 648 平台配置ART優化
(1)device/mstar/almond/BoardConfig.mk 中配置:
WITH_DEXPREOPT := true
看一下 WITH_DEXPREOPT 控制流程:build/core/dex_preopt_odex_install.mk
# Setting LOCAL_DEX_PREOPT based on WITH_DEXPREOPT, LOCAL_DEX_PREOPT, etc LOCAL_DEX_PREOPT := $(strip $(LOCAL_DEX_PREOPT)) ifneq (true,$(WITH_DEXPREOPT)) LOCAL_DEX_PREOPT := else # WITH_DEXPREOPT=true ifeq (,$(TARGET_BUILD_APPS)) # TARGET_BUILD_APPS empty ifndef LOCAL_DEX_PREOPT # LOCAL_DEX_PREOPT undefined ifneq ($(filter $(TARGET_OUT)/%,$(my_module_path)),) # Installed to system.img. ifeq (,$(LOCAL_APK_LIBRARIES)) # LOCAL_APK_LIBRARIES empty # If we have product-specific config for this module? ifeq (disable,$(DEXPREOPT.$(TARGET_PRODUCT).$(LOCAL_MODULE).CONFIG)) LOCAL_DEX_PREOPT := false else LOCAL_DEX_PREOPT := $(DEX_PREOPT_DEFAULT) endif else # LOCAL_APK_LIBRARIES not empty LOCAL_DEX_PREOPT := nostripping endif # LOCAL_APK_LIBRARIES not empty endif # Installed to system.img. endif # LOCAL_DEX_PREOPT undefined endif # TARGET_BUILD_APPS empty endif # WITH_DEXPREOPT=true
以上邏輯過濾各種條件,會執行LOCAL_DEX_PREOPT := $(DEX_PREOPT_DEFAULT)語句。
其中 LOCAL_DEX_PREOPT 的賦值 :nostripping - 優化但不剝離apk中的classes.dex true - 優化並剝離APK中的classes.dex false - 不進行優化。
(2)DEX_PREOPT_DEFAULT的定義在 device/mstar/almond/BoardConfigCommon.mk 中:
DEX_PREOPT_DEFAULT := true
LOCAL_DEX_PREOPT為true時將會按照build/core/dex_preopt.mk文件相應的規則優化APK文件。
#################################### # dexpreopt support - typically used on user builds to run dexopt (for Dalvik) or dex2oat (for ART) ahead of time # #################################### # list of boot classpath jars for dexpreopt DEXPREOPT_BOOT_JARS := $(subst $(space),:,$(PRODUCT_BOOT_JARS)) DEXPREOPT_BOOT_JARS_MODULES := $(PRODUCT_BOOT_JARS) PRODUCT_BOOTCLASSPATH := $(subst $(space),:,$(foreach m,$(DEXPREOPT_BOOT_JARS_MODULES),/system/framework/$(m).jar)) PRODUCT_SYSTEM_SERVER_CLASSPATH := $(subst $(space),:,$(foreach m,$(PRODUCT_SYSTEM_SERVER_JARS),/system/framework/$(m).jar)) DEXPREOPT_BUILD_DIR := $(OUT_DIR) DEXPREOPT_PRODUCT_DIR_FULL_PATH := $(PRODUCT_OUT)/dex_bootjars DEXPREOPT_PRODUCT_DIR := $(patsubst $(DEXPREOPT_BUILD_DIR)/%,%,$(DEXPREOPT_PRODUCT_DIR_FULL_PATH)) DEXPREOPT_BOOT_JAR_DIR := system/framework DEXPREOPT_BOOT_JAR_DIR_FULL_PATH := $(DEXPREOPT_PRODUCT_DIR_FULL_PATH)/$(DEXPREOPT_BOOT_JAR_DIR) # The default value for LOCAL_DEX_PREOPT DEX_PREOPT_DEFAULT ?= true #如果之前沒有被賦值則=true # $(1): the .jar or .apk to remove classes.dex define dexpreopt-remove-classes.dex $(hide) zip --quiet --delete $(1) classes.dex; \ dex_index=2; \ while zip --quiet --delete $(1) classes$${dex_index}.dex > /dev/null; do \ let dex_index=dex_index+1; \ done endef ...
(3)如果想跳過指定的APK不進行優化,則在 build/core/dex_preopt_odex_install.mk 修改規則,如跳過 QuickApp.apk 不進行優化:
# Skip QuickPic DEOEX avoid "Didn't find class "com.alensw.PicFolder.QuickApp" ifeq ($(LOCAL_MODULE),QuickPic) LOCAL_DEX_PREOPT:= endif
(4) 如果編譯jar包不想進行優化,可以修改 build/core/java_library.mk 中 LOCAL_DEX_PREOPT := false ,全部重新編譯后就可以mm單獨編譯jar包進行調試。
(5) 如果發現 LOCAL_DEX_PREOPT:=true時 apk中的.dex無法剔除出來,需要關注另一個參數 LOCAL_CERTIFICATE,作用是指定用於哪個key簽名,未指定的默認用testkey:
LOCAL_CERTIFICATE := testkey #普通APK,默認情況下使用。 LOCAL_CERTIFICATE := platform #該APK完成一些系統的核心功能,經過對系統中存在的文件夾的訪問測試,這種方式編譯出來的APK所在進程的UID為system。 LOCAL_CERTIFICATE := shared #該APK需要和home/contacts進程共享數據。 LOCAL_CERTIFICATE := media #該APK是media/download系統中的一環。
LOCAL_CERTIFICATE := PRESIGNED #表示apk已經簽過名了,系統不需要再次簽名,為了保持第三方的簽名有效,不會剝離.dex文件。
5.項目中執行的效果
優化前:
優化后:
對比可發現,沒有預優化的apk文件有個classes.dex文件,而預優化的沒有這個文件了,java代碼(classes.dex)到了oat子目錄中。因此,在user環境下編譯,生成的apk文件是不能直接上傳到服務器的,因為這個apk已經沒有java的代碼了。所以在編譯apk時,把mk文件中添加LOCAL_DEX_PREOPT := false把預編譯關閉。
如果在user下編譯的apk,push到daily的系統中,apk依然能夠運行,不是沒有classes.dex文件了嗎?java代碼都沒有了,為什么還能運行?在第三節可知道,在android系統啟動是會優化系統的app,所以daily中的APK的java代碼被緩存到手機本地了,push user下編譯的apk運行時依然運行的是daily版中apk緩存下來的code。