关于dex重排

关于dex重排

Android 编译打包与 Dex 重排说明

1. 文档目的

这篇文档用于整理 Android 应用从源码到 APK 的主要编译打包流程,并说明 dex 重排的原理、典型执行步骤,以及当前公开可参考的主流方案。

本文重点回答三个问题:

  1. Android 应用是如何从 .kt / .java 源码一步步生成 APK 的。
  2. dex 重排到底在优化什么,通常如何落地。
  3. 公开资料里有哪些现有方案,它们和企业内部工具大致是什么关系。

2. Android 打包编译流程

从整体上看,一个 Android 应用从源码到可安装产物,通常会经历下面几个阶段。

2.1 源码编译阶段

开发者编写的 Kotlin / Java 源码,首先会分别经过 kotlincjavac 编译,生成 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 编译阶段

这是和本文最相关的阶段。

在这一步里,d8r8 会把前面生成的 .class / .jar 转换成 Android 运行时可识别的 .dex 文件。

可以把 d8 理解成 Android 构建链路中的 dex 编译器,它通常位于:

  • Java / Kotlin 编译之后
  • APK 打包之前

它的主要职责包括:

  • .class 转换为 .dex
  • 处理 desugar,把部分较新的 Java 语言特性转换成低版本 Android 能接受的形式。
  • 参与多 dex 划分,例如生成 classes.dexclasses2.dexclasses3.dex
  • 在给定 main-dex-liststartup-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.dexclasses2.dex,那么后面的 dex 就有机会在启动阶段不被访问,从而降低:

  • 启动期 I/O
  • dex / vdex 相关 mmap
  • code 内存占用
  • page cache 污染

3.3 为什么 d8 可以参与 dex 重排

d8 能参与 dex 重排,并不是因为它会“原地修改旧 dex 文件”,而是因为它本来就是一个重新生成 dex 的编译器。

也就是说,它的工作方式更接近:

  1. 读取输入 class / dex 信息。
  2. 建立内部表示。
  3. 根据约束决定哪些类应进入哪个输出 dex。
  4. 重新产出新的 classes.dexclasses2.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 识别的输入格式。

最典型的是两类:

  1. main-dex-list
  2. startup-profile

其中:

  • main-dex-list 更像强约束,指定这些类优先进入前面的 dex。
  • startup-profile 更像布局提示,用于指导构建系统优化启动期访问局部性。

4.7 调用 d8 / r8 重新生成 dex

这是 dex 重排真正落地的一步。

工具会把:

  • 原 APK 中提取出的 dex
  • 生成好的 main-dex-liststartup-profile
  • 对应的构建工具路径

一起交给 d8r8,让它重新输出一套新的 dex 布局。

需要注意的是,这里通常不是“直接改旧 dex 二进制”,而是“重新生成一套新的 dex 文件”。

4.8 回填 APK、对齐、签名

新的 dex 文件生成之后,还需要:

  • 回填到 APK 中
  • 重新 zipalign
  • 重新签名

到这一步,重排后的 APK 才能真正用于安装和验证。

4.9 验证是否生效

验证通常至少包括两层:

  1. 结构验证
  • 重新抓取运行期使用类
  • 确认关键类是否主要集中在前几个 dex 中
  1. 效果验证
  • 观察 dex / vdex mmap 相关内存变化
  • 观察启动耗时、关键路径性能是否改善

如果关键类依然逃逸到较后的 dex,或者场景覆盖不足,那么重排收益往往会明显下降。

5. 一个企业内部工具的大致实现思路

根据前面讨论过的内部文档特征,这类 dex_re_layout.jar 风格的工具,大概率不是“直接手写 dex 二进制修改器”,而是一个围绕以下步骤组织起来的工程化流水线工具:

  1. 读取 trace / used_class
  2. 通过 mapping.txt 做反混淆或再混淆
  3. 解析 APK 中 dex 结构并统计类容量信息
  4. 按优先级与容量约束生成 main-dex-liststartup-profile
  5. 调用 d8 完成新的 dex 生成
  6. 将新 dex 回填 APK,并进行对齐与签名

因此,这类工具的核心价值通常不在“自己重写 dex 格式”,而在:

  • 采样数据整理
  • 名字映射
  • 约束规划
  • 工具链调度
  • 工程化落地

6. 网上现有方案情况

当前公开可参考的方案,主要集中在两条路线:

  1. Meta 的 Redex / InterDex
  2. 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 风格工具,则更像是在这些通用思想之上,结合内部采样、映射、规划、打包和签名流程做出的工程化实现。