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。