序、众所周知,每一次开始写这种框架系类的文章的时候,就是我在找工作了,有闲暇时间可以写点东西。哈哈哈哈,26年找工作,简直就是。。。。。
重要的事情说三遍
有岗位的请 CALL 我,谢谢大家了。。。。
有岗位的请 CALL 我,谢谢大家了。。。。
有岗位的请 CALL 我,谢谢大家了。。。。
前面三篇,我们已经分别讲了:
-
APK 结构与体积分析
-
DEX、R8 与依赖治理
-
Resources、图片、SO、Assets 优化
这些基本覆盖了 Android 包体积优化中最常见的手段。
但如果:
R8 已经开启,依赖也清理过了,DEX 仍然占整个 APK 很高比例,还能不能继续优化?
这时候就可以继续往更深一层走:
DEX Bytecode Optimization。
这里我实际使用的是:
ReDex。
一、ReDex 是什么?
ReDex 是 Meta 开源的一套 Android DEX 优化工具。
它和普通 APK 分析工具不一样。
APK Analyzer 更多是:
看 APK 里面有什么。
ReDex 则会真正:
读取 DEX → 分析 DEX → 修改 DEX → 重新生成 APK。
可以简单理解成:
APK
↓
解析 DEX
↓
建立内部代码结构
↓
执行多个 Optimization Pass
↓
重新布局 / 修改 DEX
↓
重新输出 APK
所以 ReDex 属于:
DEX 后处理优化工具。
二、已经有 R8 了,为什么还需要 ReDex?
这是最容易被问的问题。
R8 本身已经能够完成:
-
Shrinking
-
Optimization
-
Obfuscation
-
DEX 生成
所以:
ReDex 并不是用来替代 R8。
两者更适合理解成:
R8
位于 Android 正常 Build Pipeline 中。
源码 / Bytecode
↓
R8
↓
DEX
↓
APK
而 ReDex:
是在 APK / DEX 已经产生以后继续处理。
流程变成:
R8
↓
APK
↓
ReDex
↓
Optimized APK
所以更准确地说:
R8 是 Android 构建链中的主优化器,ReDex 可以作为后处理层继续寻找 DEX 优化空间。
三、ReDex 的核心:Pass
ReDex 并不是:
一个算法把 APK 自动变小。
它更像一个:
Optimization Pass Framework。
不同 Pass:
负责不同事情。
例如:
某个 Pass:
处理 Debug 信息。
某个 Pass:
调整 DEX 中 Class 的布局。
某个 Pass:
做代码分析。
所以使用 ReDex 时,真正需要关注的是:
到底开启了哪些 Pass。
Pass 开得越多:
不一定越好。
因为 Bytecode 修改越激进:
潜在风险也越高。
所以我在实际测试时,没有一开始就堆很多 Pass。
目前只选择了:
StripDebugInfoPass
InterDexPass
先建立一个相对容易验证的优化组合。
四、StripDebugInfoPass
DEX 中除了真正执行的 Bytecode,还存在一些:
Debug Metadata。
例如:
-
Source File 信息
-
Line Number
-
Local Variable 信息
-
Debug Position 信息
这些数据:
对于开发和 Crash 定位非常有价值。
但从 App 正常运行角度来看:
很多 Debug Metadata 并不是业务逻辑本身。
所以:
StripDebugInfoPass
可以根据配置:
删除或精简部分 DEX Debug 信息。
最终效果就是:
DEX Size ↓
APK Size ↓
五、StripDebugInfo 的代价
这里一定要注意:
Debug 信息不是越少越好。
因为如果过度删除:
线上 Crash Stack Trace:
可能变得更难定位。
所以 Release 包优化时:
应该在:
包体积
和:
可诊断性
之间取得平衡。
也就是说:
StripDebugInfo 不是简单:
把所有 Debug 信息全删掉。
而应该根据项目线上 Crash 分析需求选择配置。
六、InterDexPass 是什么?
如果说:
StripDebugInfo
主要解决:
DEX 里面哪些信息可以减少。
那么:
InterDex
更关注:
Class 应该怎么放进不同的 DEX。
对于 MultiDex 应用:
可能存在:
classes.dex
classes2.dex
classes3.dex
......
Class 怎么分布:
并不是完全无所谓。
例如某些启动阶段经常使用的 Class:
如果分散在很多 DEX 中:
可能导致更差的访问局部性。
所以 InterDex 的核心可以理解成:
重新调整 Class Ordering 和 Cross-DEX Distribution。
七、InterDex 不是"减少 DEX 数量工具"
这个概念要特别说一下。
很多人看到我的实际结果:
DEX:
7 → 5
很容易理解成:
InterDex 的作用就是减少 DEX 数量。
其实不准确。
InterDex 真正解决的是:
DEX Layout。
也就是:
哪些 Class 放在哪个 DEX。
最终 DEX 数量:
可能减少
也可能不变
甚至在某些策略下出现不同变化。
所以:
7 → 5 是这次实际测试结果,不是 InterDex 的固定效果。
八、为什么 DEX Layout 值得优化?
假设 App 冷启动经常访问:
Class A
Class B
Class C
Class D
原来的布局:
DEX1:
A
DEX4:
B
DEX6:
C
DEX2:
D
那么启动阶段可能需要访问多个 DEX。
如果经过合理布局:
DEX1:
A
B
C
D
启动所需 Class:
更加集中。
这种:
Locality
就是 DEX Layout 优化的重要方向之一。
所以 InterDex 的意义并不只是:
包体积。
它还可能和:
启动性能
I/O
运行时访问模式
产生关系。
九、我的实际测试结果
为了测试 ReDex,我准备了一个故意引入大量依赖的 Android 项目。
其中包括:
-
FastUtil
-
Guava
-
BouncyCastle
-
Protobuf
-
Tink
原始 APK:
12,557,258 Bytes
大约:
12.56 MB
DEX:
7 个
Class Defs:
20,357
Method ID Entries:
273,702
然后执行:
StripDebugInfoPass
InterDexPass
ReDex 整个优化过程:
大约耗时:
53.7 秒
最终:
Optimized APK:
10,350,190 Bytes
DEX:
5 个
Class Defs:
20,362
Method ID Entries:
275,958
最终减少:
2,207,068 Bytes
体积下降:
17.58%
DEX:
7 → 5
对于这次实验来说:
效果还是比较明显的。
十、一个很有意思的问题:Method ID Entries 为什么反而增加了?
这个结果特别值得单独拿出来讲。
优化前:
273,702
优化后:
275,958
也就是说:
APK:
小了。
DEX:
少了。
但是:
Method ID Entries:
反而增加了。
是不是优化失败了?
不是。
十一、原因在于 Method ID Table 是 per-dex 的
前面第二篇讲过:
每一个 DEX:
都有自己的:
Method ID Table。
所以:
classes.dex
有自己的 Method References。
classes2.dex
也有自己的。
当 InterDex 重新调整:
Class Distribution
以后:
不同 DEX 中需要引用的方法集合:
也会变化。
例如:
以前 Class A 和 Class B:
在同一个 DEX。
后来:
被重新放到两个 DEX。
那么:
不同 DEX:
可能分别需要保存相关 Method Reference。
于是:
所有 DEX 的:
method_ids_size
加起来:
完全可能出现增加。
十二、这说明包体积优化不能只盯一个指标
如果只看:
Method ID Entries:
那么会得出:
优化以后更差了。
但实际上:
APK:
减少:
17.58%。
所以真正判断优化效果:
应该综合看:
-
APK Size
-
DEX Total Size
-
DEX Count
-
Class Defs
-
Method ID Entries
-
Runtime Correctness
而不是:
只看某一个数字。
这也是做 APK 优化比较容易踩的一个坑。
十三、ReDex 优化完成就算成功了吗?
还不能。
ReDex:
Successfully Finished
只能说明:
DEX 重写完成了。
它不能保证:
修改后的 APK 一定能正常运行。
Bytecode 被修改以后:
理论上可能出现:
-
VerifyError
-
ClassNotFoundException
-
NoClassDefFoundError
-
Startup Crash
-
Runtime Crash
所以:
APK 变小只是第一步。
十四、必须进行 APK Validation
我最终没有只看:
APK Size。
还继续进行了:
ZIP Alignment 检查
↓
APK Signature 检查
↓
真机安装
↓
App 启动
↓
PID 检查
↓
Logcat 检查
最终优化后的 App:
成功安装并启动。
执行:
pidof
可以正常获得 App Process。
同时没有发现 App 自身的:
-
FATAL EXCEPTION
-
VerifyError
-
NoClassDefFoundError
-
Startup Crash
到这个阶段:
我才认为这组优化:
基本验证通过。
十五、真机验证还踩了一个坑
Logcat 中:
出现了:
ClassNotFoundException
第一眼看:
很容易怀疑:
ReDex 把 Class 搞丢了。
结果继续分析发现:
这个异常来自:
MIUI 系统进程。
而不是:
目标 App。
这说明:
自动验证不能简单写成:
发现:
ClassNotFoundException
↓
直接失败。
必须结合:
-
package
-
PID
-
process
-
AndroidRuntime
-
stack trace
-
startup time window
判断:
异常到底是不是目标 App 的。
这个问题也直接引出了我后续准备做的:
ADB 自动 Validation。
十六、什么时候应该考虑 ReDex?
我不建议一上来:
就使用 ReDex。
比较合理的顺序是:
APK Analyze
↓
R8
↓
Keep Rule 优化
↓
Dependency 治理
↓
Resources / SO / Assets 优化
↓
重新 Analyze
↓
如果 DEX 仍然存在较明显的优化空间
↓
再考虑 ReDex。
因为:
最好的优化往往不是:
用复杂算法处理无用代码。
而是:
直接删除无用代码和依赖。
十七、总结
ReDex 可以理解成:
APK / DEX 构建完成之后的一层 Bytecode Optimization。
它不是:
R8 的替代品。
而是:
更深一步的 DEX 后处理工具。
这次实际测试主要使用:
StripDebugInfoPass
负责:
精简 DEX Debug Metadata。
InterDexPass
负责:
优化 Class Ordering 和 DEX Distribution。
最终:
12.56 MB → 10.35 MB
体积下降:
17.58%
DEX:
7 → 5
但更重要的不是:
"减少了 17.58%。"
而是让我进一步认识到:
包体积优化不能只关注 APK 是否变小,还必须关注优化后的 APK 是否仍然正确。
所以真正完整的优化流程应该是:
Analyze
↓
Optimize
↓
Re-Analyze
↓
Compare
↓
Validate
而不是:
APK 变小
↓
结束。
下一篇
《Android 包体积优化(五):从人工优化到自动化------Gradle Plugin、真机验证与 CI Gate》
最后一篇会把整个系列串起来:
从人工使用 APK Analyzer,
发展到:
Gradle Plugin
↓
自动 APK Analysis
↓
Diagnosis
↓
ReDex
↓
Compare
↓
ADB Validation
↓
CI Gate
真正把包体积优化从:
一次性专项
变成:
长期工程化治理。