编译策略:AOT编译、JIT编译、混合编译模式
说到Android Runtime的编译策略,这其实是个很有意思的话题。我刚开始接触ART时,总觉得编译不就是把代码变成机器码吗?后来才发现,这里面的门道比我想象的深得多。
Android的编译策略,说白了就是解决一个核心矛盾:安装快还是运行快。你想想看,如果所有代码都在安装时编译好(AOT),安装慢但运行快;如果运行时再编译(JIT),安装快但启动慢。Google在这条路上折腾了好几个大版本,最终找到了一个平衡点。
一、AOT编译(预编译)
AOT,全称是Ahead-Of-Time,提前编译。在Android 5.0的ART中,Google首次引入了AOT编译。应用安装时,dex2oat工具会把DEX字节码一次性编译成本地机器码。
核心特点:
- 安装时编译,生成.oat文件
- 启动速度快,无需解释执行
- 运行性能稳定,没有编译开销
- 安装时间长,占用存储空间大
我记得在Android 5.0刚出来时,有个用户反馈说「为什么我装个微信要等5分钟?」。嗯,这就是AOT的代价。每个应用安装都要完整编译,大型游戏甚至可能编译十几分钟。
注意:AOT编译生成的机器码体积通常是DEX的2-3倍。对于存储空间紧张的低端机,这是个不小的负担。
二、JIT编译(即时编译)
到了Android 7.0,Google又回到了JIT模式。JIT(Just-In-Time)编译只在应用运行时才编译热点代码。没被调用的方法,就一直保持字节码形式。
为什么会这样?因为Google发现,大部分应用里只有20%的代码是频繁执行的。为那80%的冷门代码做AOT编译,完全是浪费。
JIT的工作流程:
- 应用启动,解释执行字节码
- ART运行时统计方法调用次数
- 达到阈值(默认10000次),触发JIT编译
- 编译后的机器码缓存到code cache
- 下次调用直接执行机器码
我曾经在项目中遇到过一个问题:某个第三方SDK在启动时有个循环,每次循环都调用同一个方法。JIT编译后,启动时间从3秒降到了0.8秒。这就是JIT的威力——它只编译真正需要优化的代码。
小技巧:JIT编译的阈值可以通过系统属性调整。但我不建议你乱改,默认值已经经过Google大量测试,是性能和内存的平衡点。
三、混合编译模式
从Android 8.0开始,ART引入了混合编译模式。这是目前最成熟的方案,也是我日常工作中接触最多的。
混合编译的核心思路是:JIT编译热点代码,然后在设备空闲时,把JIT生成的profile信息用于AOT编译。说白了,就是让JIT先跑一遍,摸清哪些代码是热点,然后后台悄悄地把这些热点代码AOT编译掉。
ART提供了三种编译模式,通过pm compile命令可以切换:
| 编译模式 | 命令参数 | 特点 | 适用场景 |
|---|---|---|---|
| Speed | --compile-filter=speed |
全量AOT编译,性能最佳 | 系统应用、预装应用 |
| Speed-profile | --compile-filter=speed-profile |
基于profile的混合编译 | 大多数第三方应用 |
| Interpret-only | --compile-filter=interpret-only |
纯解释执行,不编译 | 调试、测试场景 |
1. Speed模式
Speed模式就是全量AOT编译。所有方法都会被编译成本地机器码,不管它是不是热点。这种模式下,应用启动最快,运行最流畅。
但代价也很明显:安装时间长,存储占用大。我记得有一次给客户做系统优化,把预装应用的编译模式从Speed改成Speed-profile后,系统分区直接省出了500MB空间。
2. Speed-profile模式
这是我最推荐的模式,也是Google默认使用的方案。它的工作流程是这样的:
1. 应用安装后,先以JIT模式运行
2. ART收集运行时的profile信息
- 记录哪些方法被频繁调用
- 记录哪些类被加载
- 记录哪些代码路径被执行
3. 设备空闲且充电时,后台进程background dexopt启动
4. 根据profile信息,只编译热点方法
5. 下次启动时,热点代码直接执行机器码
你想想看,这个方案多聪明。它既保留了JIT的快速安装,又通过后台编译实现了接近AOT的运行性能。我在项目中做过对比测试:Speed-profile模式下的应用启动速度,只比全量AOT慢了不到5%,但安装时间缩短了80%。
关键点:Speed-profile模式依赖profile的准确性。如果用户的使用习惯和profile收集时的场景差异很大,热点代码可能没被编译,导致性能下降。
3. Interpret-only模式
这个模式比较特殊,它完全不做编译,所有代码都解释执行。你可能会问:这有什么用?
嗯,我在调试ART内部问题时经常用这个模式。比如怀疑某个编译优化有bug,切换到Interpret-only模式就能确认问题是否出在编译环节。另外,在开发阶段,这个模式也能加快迭代速度——毕竟不用等编译。
注意:Interpret-only模式下性能很差,千万不要在生产环境使用。我曾经见过有人把这个模式配置到正式发布版本上,结果应用卡得用户想摔手机。
四、编译策略的选择
在实际项目中,怎么选择编译策略?我个人的经验是这样的:
- 系统预装应用:用Speed模式。这些应用用户天天用,值得全量编译。
- 第三方应用:用Speed-profile模式。Google默认就是这个,别乱改。
- 调试/测试版本:用Interpret-only模式。加快开发效率。
- 特殊场景:比如车载系统、IoT设备,可以根据硬件情况定制。
我曾经在一个车载项目中,因为存储空间极其有限(只有2GB),不得不把所有应用的编译模式改成Speed-profile,并且把profile收集周期从7天缩短到3天。效果还不错,启动速度只慢了10%,但存储占用减少了60%。
五、知识体系总览
下面这张图总结了Android编译策略的完整知识体系,你可以对照着理解:
这张图把三种编译模式的关系和特点都展示清楚了。你可以看到,混合编译其实是JIT和AOT的结合体,它吸收了两种模式的优点。
六、实际项目中的避坑指南
最后,分享几个我在项目中踩过的坑:
避坑1:我曾经在某个项目中,为了追求极致性能,把所有应用都设置成Speed模式。结果用户反馈手机存储空间不够用。后来改成Speed-profile,问题解决了,性能几乎没有下降。
避坑2:有次调试一个ANR问题,我怀疑是JIT编译导致的。用Interpret-only模式跑了一遍,ANR消失了。后来发现是JIT编译时触发了某个锁竞争。嗯,这种问题最难排查。
避坑3:Speed-profile模式依赖profile文件。如果用户很少使用某个功能,这个功能的代码就不会被编译。我建议在应用的关键路径上主动触发一次执行,让profile能覆盖到。
好了,关于编译策略的内容就到这里。记住一点:没有最好的编译模式,只有最适合你场景的模式。理解每种模式的原理和代价,你就能做出正确的选择。