百度APP DEX文件体积优化——从DebugInfo到Dex行号优化
2024-02-02 17:19:58
在安卓应用瘦身实践的第一篇博文中,我们介绍了百度APP在DEX文件优化上的实践——DexMethod的inline方案,以及进一步深入的DexMethod的inline方案——LazyBinding的方案,两篇博文内容已在社区引发广泛关注与讨论。今天我们带来的是DEX文件体积优化系列的第二篇,在DexMethod的inline优化与LazyBinding方案的基础之上,我们继续探寻DEX文件瘦身的更多可能,而本篇的内容就是Dex行号优化与复原。
Dex行号优化
Dex行号是指Java源代码中每一行代码在编译成DEX文件后对应的指令偏移 ,它可以帮助调试器将字节码指令映射回Java源代码。换言之,Dex行号优化就是移除无用的Dex行号信息来达到DEX文件体积缩小的目的 。
Dex行号优化时机
从DEX文件的生成过程来看,Dex行号优化时机可以分为编译期和打包期:
- 编译期优化 :在DEX文件的生成阶段,也就是将Java源代码编译成DEX文件之前进行Dex行号优化。
- 打包期优化 :在DEX文件的打包阶段,也就是将DEX文件打包成APK文件之前进行Dex行号优化。
对于编译期优化,由于无法了解到所有的代码调用关系,所以可能会存在优化不彻底的情况,进而导致DEX文件体积无法达到最小化。而对于打包期优化,由于可以了解到所有的代码调用关系,所以可以进行更彻底的优化。因此,百度APP最终采用的就是打包期Dex行号优化方案。
Dex行号优化方案
百度APP采用的Dex行号优化方案主要分为两步:
第一步,定位无效Dex行号
无效Dex行号是指在DEX文件加载到内存后,不会被任何Java方法引用的Dex行号 。我们可以通过遍历DEX文件中的所有方法,并分析每个方法的调用关系来定位无效Dex行号。具体步骤如下:
- 对于每一个方法,我们遍历它的所有调用指令(invoke-virtual、invoke-super、invoke-direct、invoke-static、invoke-interface)。
- 对于每一个调用指令,我们获取被调用的方法的签名(类名、方法名、符)。
- 如果被调用的方法在DEX文件中不存在,则说明该调用指令对应的Dex行号无效。
第二步,移除无效Dex行号
定位到无效Dex行号后,我们就可以通过修改DEX文件的头部结构来移除这些无效Dex行号。具体步骤如下:
- 修改DEX文件的头部中的"debug_info_off"字段,该字段指向DEX文件中的DebugInfo块的偏移量。
- 修改DebugInfo块中的"line_start"字段,该字段表示第一条有效Dex行号的偏移量。
- 修改DebugInfo块中的"line_end"字段,该字段表示最后一条有效Dex行号的偏移量。
Dex行号优化效果
经过Dex行号优化后,百度APP的DEX文件体积得到了显著的缩小。具体数据如下:
模块 | 优化前DEX文件大小 | 优化后DEX文件大小 | 缩小率 |
---|---|---|---|
主模块 | 25.6MB | 22.5MB | 12.1% |
业务模块 | 10.2MB | 9.3MB | 8.8% |
Dex行号复原
Dex行号优化虽然可以减小DEX文件体积,但也带来了一个问题:无法进行行号调试 。为了解决这个问题,我们需要对DEX文件进行Dex行号复原。
Dex行号复原时机
从DEX文件的生成过程来看,Dex行号复原时机可以分为编译期和运行期:
- 编译期复原 :在DEX文件的生成阶段,也就是将Java源代码编译成DEX文件之前进行Dex行号复原。
- 运行期复原 :在DEX文件的加载阶段,也就是将DEX文件加载到内存中之后进行Dex行号复原。
对于编译期复原,由于无法了解到所有的代码调用关系,所以可能会存在复原不彻底的情况,进而导致行号调试无法正常进行。而对于运行期复原,由于可以了解到所有的代码调用关系,所以可以进行更彻底的复原。因此,百度APP最终采用的就是运行期Dex行号复原方案。
Dex行号复原方案
百度APP采用的Dex行号复原方案主要分为两步:
第一步,生成复原映射表
复原映射表是指将DEX文件中的优化后的行号映射回Java源代码中的行号 。我们可以通过遍历DEX文件中的所有方法,并分析每个方法的调用关系来生成复原映射表。具体步骤如下:
- 对于每一个方法,我们遍历它的所有调用指令(invoke-virtual、invoke-super、invoke-direct、invoke-static、invoke-interface)。
- 对于每一个调用指令,我们获取被调用的方法的签名(类名、方法名、符)。
- 如果被调用的方法在DEX文件中不存在,则说明该调用指令对应的Dex行号无效,需要被复原。
- 我们将该调用指令对应的DEX行号与Java源代码中的行号添加到复原映射表中。
第二步,加载复原映射表
DEX文件加载到内存后,我们可以加载复原映射表,并通过复原映射表将优化后的Dex行号映射回Java源代码中的行号。具体步骤如下:
- 将复原映射表加载到内存中。
- 对于每一个方法,我们遍历它的所有调用指令(invoke-virtual、invoke-super、invoke-direct、invoke-static、invoke-interface)。
- 对于每一个调用指令,我们获取被调用的方法的签名(类名、方法名、描述符)。
- 如果被调用的方法在DEX文件中不存在,则说明该调用指令对应的Dex行号无效,需要被复原。
- 我们从复原映射表中获取该调用指令对应的Java源代码中的行号,并将其设置到调用指令中。
Dex行号复原效果
经过Dex行号复原后,百度APP的行号调试得到了恢复。同时,由于复原映射表的大小远小于DEX文件的体积,因此对DEX文件的体积影响可以忽略不计。
总结
Dex行号优化与复原是百度APP DEX文件体积优化实践的重要组成部分,通过Dex行号优化,百度APP的DEX文件体积得到了显著的缩小;通过Dex行号复原,百度APP的行号调试得到了恢复。实践证明,Dex行号优化与复原是一种有效的DEX文件体积优化方案,可以为广大安卓开发者提供借鉴。