关于dex重排
Android 编译打包与 Dex 重排说明
1. 文档目的
这篇文档用于整理 Android 应用从源码到 APK 的主要编译打包流程,并说明 dex 重排的原理、典型执行步骤,以及当前公开可参考的主流方案。
本文重点回答三个问题:
- Android 应用是如何从
.kt/.java源码一步步生成 APK 的。 - dex 重排到底在优化什么,通常如何落地。
- 公开资料里有哪些现有方案,它们和企业内部工具大致是什么关系。
2. Android 打包编译流程
从整体上看,一个 Android 应用从源码到可安装产物,通常会经历下面几个阶段。
2.1 源码编译阶段
开发者编写的 Kotlin / Java 源码,首先会分别经过 kotlinc 和 javac 编译,生成 JVM 字节码文件,也就是 .class 文件。
这一步的核心作用是:
- 把高级语言语法转换成 JVM 字节码。
- 处理基础的语义检查、类型检查和注解处理。
- 为后续 Android 专用的 dex 编译做准备。
此时产物还是 JVM 世界的 .class,还不是 Android 运行时直接使用的格式。
2.2 资源处理与中间产物准备阶段
除了代码本身,Android 构建过程中还会同时处理资源文件、Manifest、AIDL、DataBinding/ViewBinding 等内容,生成后续打包需要的中间产物。
典型工作包括:
aapt2编译和链接资源。- 生成
R.java/R.class等资源访问代码。 - 处理
AndroidManifest.xml。 - 合并依赖库中的 class、jar、aar 等输入。
这一阶段的结果,是把“代码 + 资源 + 依赖”整理成可供 dex 编译与 APK 打包的完整输入集合。
2.3 Dex 编译阶段
这是和本文最相关的阶段。
在这一步里,d8 或 r8 会把前面生成的 .class / .jar 转换成 Android 运行时可识别的 .dex 文件。
可以把 d8 理解成 Android 构建链路中的 dex 编译器,它通常位于:
- Java / Kotlin 编译之后
- APK 打包之前
它的主要职责包括:
- 将
.class转换为.dex。 - 处理 desugar,把部分较新的 Java 语言特性转换成低版本 Android 能接受的形式。
- 参与多 dex 划分,例如生成
classes.dex、classes2.dex、classes3.dex。 - 在给定
main-dex-list或startup-profile等约束时,影响类在多个 dex 中的分布。
如果启用了代码压缩、混淆、裁剪和优化,则通常由 r8 统一负责。可以粗略理解为:
d8:主要负责 dex 编译r8:在做 shrink / optimize / obfuscate 之后,再完成 dex 编译
2.4 APK / AAB 打包阶段
当 dex、资源、so、Manifest 等产物都准备好后,构建系统会进入打包阶段,生成 APK 或 AAB。
这个阶段的典型工作包括:
- 将
classes*.dex、资源文件、原生库、Manifest 等统一打包。 - 生成最终安装包结构。
- 为后续对齐和签名做准备。
2.5 对齐与签名阶段
最终产物还需要进行字节对齐和签名,才可以被设备正确安装和识别。
常见步骤包括:
zipalign:对 APK 做字节对齐。apksigner:进行 v1/v2/v3 等签名。
到这一步,一个可安装的 APK 才算真正构建完成。
2.6 从流程视角理解 d8 的位置
如果只看主链路,可以把 Android 编译打包流程简化成下面这条线:
.kt / .java -> kotlinc / javac -> .class -> d8 / r8 -> classes.dex / classes2.dex / ... -> APK 打包 -> zipalign -> 签名 -> 最终 APK
所以,d8 的角色不是源码编译器,也不是 APK 打包器,而是中间负责“把 JVM 字节码转换成 Android dex,并决定 dex 组织方式”的关键工具。
3. Dex 重排的原理
3.1 Dex 重排到底在优化什么
dex 重排的核心目标,不是减少源码逻辑,也不是直接提高算法执行效率,而是优化应用在启动或关键使用路径上的类访问局部性。
在默认构建链路下,类在 dex 中的分布和顺序,通常并不严格对应应用运行时真实的访问顺序。结果就是:
- 启动期要访问的类,可能散落在多个 dex 中。
- 系统在加载这些类时,需要触达更多 dex 页。
- 这会带来更多磁盘 I/O、更大的 mmap 范围,以及更高的 code / dex 相关内存开销。
dex 重排想做的事情,就是把“运行早期真正会用到的类”尽量聚合到前面的一个或几个 dex 中,减少启动阶段不必要的加载范围。
3.2 一个更直观的理解
假设应用真正启动时只需要 A、B、C、D 四类,但默认构建结果里它们分散在:
- A 在
classes.dex - B 在
classes3.dex - C 在
classes5.dex - D 在
classes7.dex
那么系统为了完成启动,可能就会间接把多个 dex 都牵扯进来。
而如果经过重排之后,把 A、B、C、D 聚合到 classes.dex 和 classes2.dex,那么后面的 dex 就有机会在启动阶段不被访问,从而降低:
- 启动期 I/O
- dex / vdex 相关 mmap
- code 内存占用
- page cache 污染
3.3 为什么 d8 可以参与 dex 重排
d8 能参与 dex 重排,并不是因为它会“原地修改旧 dex 文件”,而是因为它本来就是一个重新生成 dex 的编译器。
也就是说,它的工作方式更接近:
- 读取输入 class / dex 信息。
- 建立内部表示。
- 根据约束决定哪些类应进入哪个输出 dex。
- 重新产出新的
classes.dex、classes2.dex等文件。
因此,只要给它足够的布局约束,例如:
--main-dex-list--startup-profile
它就可以在重新生成 dex 时,改变类到 dex 的映射关系,这就是 dex 重排得以成立的基础。
4. Dex 重排的典型步骤
这里描述的是一种工程中常见、也是当前公开资料中比较典型的落地方式。
4.1 设计真实场景
首先需要定义应用的真实使用路径,尤其是对内存或启动敏感的关键场景,例如:
- 冷启动
- 首页进入
- 常用主链路操作
- 保活拉活后的典型交互
这一步非常关键,因为重排效果是否稳定,很大程度上依赖采样场景是否足够接近真实用户行为。
4.2 采集运行期使用类信息
接下来需要在真机上采集应用真实运行时使用过的类列表。
常见做法包括:
- 基于系统能力导出
used_class/ trace 信息 - 或基于其他 profile / heap dump / 运行期记录方式收集类访问数据
采集的目标不是拿到“所有类”,而是拿到“关键路径中真实使用到的类”。
4.3 类名清洗与反混淆
如果应用开启了混淆,那么采集到的类名通常是混淆后的名字,这时需要结合 mapping.txt 做反混淆,把这些类恢复成原始类名。
这一层的目的,是把“真实运行命中信息”和“工程源码语义”对齐,方便后续维护、合并和人工检查。
4.4 合并历史类列表与优先级划分
在工程化实践中,通常不会只依赖一次采样,而是会:
- 合并多个场景的类列表
- 合并多次采样结果
- 给不同场景设置优先级
例如:
- 冷启动类:最高优先级
- 首页核心交互:第二优先级
- 长尾但常见主链路:第三优先级
这样做的好处是,可以更细粒度地控制“哪些类必须最靠前,哪些类可以稍后”。
4.5 解析 APK 中现有 dex 结构
工具在真正执行重排前,往往还会先读取 APK 中的现有 dex,目的是:
- 确认目标类是否真实存在于 APK 中
- 统计每个类的方法数、字段数等信息
- 判断单个 dex 能否装下某一批类
这一步的意义在于,dex 重排不能只按“类名单”蛮干,还要满足 dex 本身的方法数、字段数等容量约束。
4.6 生成 main-dex-list 或 startup-profile
根据前面的类列表和容量约束,工具会生成可被 d8 / r8 识别的输入格式。
最典型的是两类:
main-dex-liststartup-profile
其中:
main-dex-list更像强约束,指定这些类优先进入前面的 dex。startup-profile更像布局提示,用于指导构建系统优化启动期访问局部性。
4.7 调用 d8 / r8 重新生成 dex
这是 dex 重排真正落地的一步。
工具会把:
- 原 APK 中提取出的 dex
- 生成好的
main-dex-list或startup-profile - 对应的构建工具路径
一起交给 d8 或 r8,让它重新输出一套新的 dex 布局。
需要注意的是,这里通常不是“直接改旧 dex 二进制”,而是“重新生成一套新的 dex 文件”。
4.8 回填 APK、对齐、签名
新的 dex 文件生成之后,还需要:
- 回填到 APK 中
- 重新
zipalign - 重新签名
到这一步,重排后的 APK 才能真正用于安装和验证。
4.9 验证是否生效
验证通常至少包括两层:
- 结构验证
- 重新抓取运行期使用类
- 确认关键类是否主要集中在前几个 dex 中
- 效果验证
- 观察 dex / vdex mmap 相关内存变化
- 观察启动耗时、关键路径性能是否改善
如果关键类依然逃逸到较后的 dex,或者场景覆盖不足,那么重排收益往往会明显下降。
5. 一个企业内部工具的大致实现思路
根据前面讨论过的内部文档特征,这类 dex_re_layout.jar 风格的工具,大概率不是“直接手写 dex 二进制修改器”,而是一个围绕以下步骤组织起来的工程化流水线工具:
- 读取 trace / used_class
- 通过
mapping.txt做反混淆或再混淆 - 解析 APK 中 dex 结构并统计类容量信息
- 按优先级与容量约束生成
main-dex-list或startup-profile - 调用
d8完成新的 dex 生成 - 将新 dex 回填 APK,并进行对齐与签名
因此,这类工具的核心价值通常不在“自己重写 dex 格式”,而在:
- 采样数据整理
- 名字映射
- 约束规划
- 工具链调度
- 工程化落地
6. 网上现有方案情况
当前公开可参考的方案,主要集中在两条路线:
- Meta 的
Redex / InterDex - Android 官方的
Startup Profiles
6.1 Meta Redex / InterDex
Redex 是 Meta 开源的 Android 字节码优化框架,其中 InterDex 是与 dex 布局优化最相关的一部分。
它的思路是:
- 收集运行时类加载顺序或冷启动相关类信息
- 根据这些反馈数据调整类在 dex 内和多个 dex 之间的分布
- 让启动期访问更局部化,从而减少 I/O、内存使用和 page cache 污染
它和本文讨论的 dex 重排方案最接近,因为两者都属于“基于运行时反馈进行 dex 布局优化”的路线。
可参考资料:
- Redex GitHub
- InterDex 技术文档
- Meta 关于 ReDex 的公开文章
6.2 Android 官方 Startup Profiles
Android 官方在较新的 AGP / R8 工具链中提供了 Startup Profiles 路线,用于帮助构建系统在编译时做 dex layout optimization。
它的典型特点是:
- 基于启动关键路径或基准测试生成 profile
- 交给
R8/D8在构建时消费 - 将启动相关类和方法尽量放到更适合启动期访问的位置
这条路线和企业内部的 dex 重排方案在理念上接近,但它更偏官方工具链集成,不是一个独立的“trace 解析 + dex 回填”工具。
6.3 其他公开方向
除了以上两条主线,网络上还能看到一些相关方向,但大多只是覆盖局部能力:
- 只提供 dex 解析和编辑的工具
- 只提供 main dex 控制的小工具
- 只做系统层
dex2oat/ ART 优化的项目
这些方案和完整的“采样 -> 规划 -> 重新生成 dex -> APK 回填”链路相比,通常不在同一个层级。
7. 现有方案对比
7.1 Redex / InterDex
优点:
- 是公开资料里最接近“运行时反馈驱动 dex 布局优化”的成熟方案。
- 对 dex 布局优化有较明确、系统化的技术说明。
- 更像真正的通用字节码优化框架。
局限:
- 集成和维护成本相对较高。
- 与企业内部现有构建链路结合时,可能需要额外适配。
- 公开文档与实际项目落地之间,仍存在工程细节差距。
7.2 官方 Startup Profiles
优点:
- 官方支持,和 AGP / R8 工具链集成度高。
- 对现代 Android 项目更友好,维护成本通常更低。
- 不需要单独维护一套复杂的 dex 回填流水线。
局限:
- 更偏启动路径优化,不完全等价于企业内部的自定义 dex 重排工具。
- 自定义空间相对有限。
- 与“多场景 used_class 合并、优先级规划、内部流水线调度”这种深度工程化方案并不完全一致。
7.3 企业内部 dex_re_layout 风格工具
优点:
- 可以与内部系统能力、采样方式、签名流程、流水线深度集成。
- 可以更精细地控制类列表来源、优先级和生成流程。
- 更容易结合具体业务场景做定制。
局限:
- 维护成本高。
- 依赖内部环境和工具链。
- 可移植性与通用性通常不如开源通用框架。
8. 结论
Android 构建流程里,d8 / r8 处于源码编译之后、APK 打包之前,负责把 JVM 字节码转换成 Android 可运行的 dex,并参与多 dex 划分与部分 dex 布局控制。
dex 重排的本质,不是直接修改业务逻辑,而是根据真实运行路径或启动 profile,把关键类尽量聚合到前面的一个或几个 dex 中,降低启动期 I/O、mmap 与内存开销。
从公开资料来看,最接近这种思路的开源方案是 Meta 的 Redex / InterDex;Android 官方则提供了更现代、更集成的 Startup Profiles 路线。企业内部的 dex_re_layout.jar 风格工具,则更像是在这些通用思想之上,结合内部采样、映射、规划、打包和签名流程做出的工程化实现。