作者:字节跳动终端技术——郭海洋
背景
对于Android App
的性能优化来说,方式方法以及工具都有很多,而dex2oat
作为其中的一员,却可能不被大众所熟知。它是Android
官方应用于运行时,针对dex
进行编译优化
的程序,通过对dex
进行一系列的指令优化、编译机器码等操作,提升dex加载速度
和代码运行速度
,从而提升安装速度、启动速度、以及应用使用过程中的流畅度,最终提升用户日常的使用体验。
它的适用范围也比较广,可以用于Primary Apk
和Secondary Apk
的常规场景
和插件场景
。(Primary Apk
是指的常规场景
下的主包
(base.apk
)或者插件场景
下的宿主包
,Secondary Apk
是指的常规场景
下的自行加载的包
(.apk
)或者插件场景
下的插件包
(.apk
))。
而随着Android
系统版本的更迭,发现原本可以在应用进程
上触发dex2oat
编译的方式,却在targetSdkVersion>=29
且Android 10+
的系统上,不再允许使用。其原因是系统在targetSdkVersion=29
的时候,对此做了限制,不允许应用进程
上触发dex2oat
编译(Android 运行时 (ART) 不再从应用进程调用 dex2oat。这项变更意味着 ART 将仅接受系统生成的 OAT 文件)(OAT
为dex2oat
后的产物)。
那当前是否会受到这个限制的影响呢?
在2020
年的时候Android 11
系统正式发布,各大应用市场就开始限制App
的targetSdkVersion>=29
,而Android 11
系统距今已经发布一年之久,也就意味着,现如今App
的targetSdkVersion>=29
是不可避免的。而且随着新Android
设备的不断迭代,越来越多的用户,使用上了携带新系统的新机器,使得Android 10+
系统的占有量逐步增加,目前为止Android 10+
系统的占有量约占整体的30%~40%
左右,也就是说这部分机器将会受到这个限制的影响。
那这个限制有什么影响呢?
这个限制的关键是,不允许应用进程
上触发dex2oat
编译,换句话说就是并不影响系统自身去触发dex2oat
编译,那么限制的影响也就是,影响那些需要通过应用进程
去触发dex2oat
编译的场景。
对于Primary Apk
和Secondary Apk
,它们在常规场景
和插件场景
下,系统都会收集其运行时的热点代码
并用于dex2oat
进行编译优化
。此处触发dex2oat
编译是系统行为,并不受限于上述限制。但触发此处dex2oat
编译的条件是比较苛刻的,它要求设备必须处于空闲状态且要连接电源,而且其校验的间隔是一天。
在上述条件下,由系统触发的dex2oat
编译,基本上很难触发,从而导致dex加载速度
下降80%
以上,代码运行速度
下降11%
以上,使得应用的ANR
率提升、流畅度下降,最终影响用户的日常使用体验。
对于之前来说改进方案就是通过应用进程
触发dex2oat
编译来弥补系统触发dex2oat
编译的不足,而如今因限制会导致部分机器无法生效。
如何才能让用户体会到dex2oat
带来的体验提升呢?问题又如何解决呢?
下面通过探索,一步步的逼近真相,解决问题~
探索
探索之前,先明确下核心点,本次探索的目标就是为了让用户体会到dex2oat
带来的体验提升,其最大的阻碍就是系统触发dex2oat
的编译条件太苛刻,导致难以触发,之前的成功实践就是基于App维度
手动触发dex2oat
编译来弥补系统触发dex2oat
的编译的不足。
而现在仍需探索的原因就是,原本的成功实践,目前在某些机器上已经受限,为了完成目标,解决掉现有的问题,自然而然的想法就是,限制究竟是什么?限制是如何生效的?是否可以绕过?
限制是什么?
目前对于限制的理解,应该仅限于背景中的描述,那Google官方
是怎么说的呢?
Android 运行时 (ART) 不再从应用进程调用
dex2oat
。这项变更意味着 ART 将仅接受系统生成的 OAT 文件。(Android 运行时只接受系统生成的 OAT 文件)
通过Google官方
的描述大致可以理解为,原本ART
会从应用进程调用dex2oat
,现在不再从应用进程调用dex2oat
了,从而使得应用进程没有时机触发dex2oat
,从而达到限制App维度
触发dex2oat
的目的。
但问题确实有这么简单嘛?
通过对比Android 9
和 Android 10
的代码时发现,Android 9
在构建ClassLoader
的时候会触发dex2oat
,但是 Android 10
上相关代码已经被移除,此处同Google官方
的说法一致。
但如果限制仅仅如此的话,可以按照原本ART
从应用进程调用dex2oat
的方式,然后手动从应用进程调用就可以了。
由于Android`` ``10
相关代码已经移除,所以查看下Android 9
的代码,看下之前是如何从应用进程调用dex2oat
的,相关代码链接:https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r52/runtime/oat_file_assistant.cc#698,通过查看代码可以看出,是通过拼接dex2oat
的命令来触发执行的,按照如上代码,拼接dex2oat
命令的伪代码如下:
//step1 拼接命令
List<String> commandAndParams = new ArrayList<>();
commandAndParams.add("dex2oat");
if (Build.VERSION.SDK_INT >= 24) {
commandAndParams.add("--runtime-arg");
commandAndParams.add("-classpath");
commandAndParams.add("--runtime-arg");
commandAndParams.add("&");
}
commandAndParams.add("--instruction-set=" + getCurrentInstructionSet());
// verify-none|interpret-only|verify-at-runtime|space|balanced|speed|everything|time
//编译模式,不同的模式,影响最终的运行速度和磁盘大小的占用
if (mode == Dex2OatCompMode.FASTEST_NONE) {
commandAndParams.add("--compiler-filter=verify-none");
} else if (mode == Dex2OatCompMode.FASTER_ONLY_VERIFY) {
//快速编译
if (Build.VERSION.SDK_INT > 25) {
commandAndParams.add("--compiler-filter=quicken");
} else