ODEX(Optimized DEX)和 VDEX(Verifiable DEX)是 Android 运行时性能的关键组件。从 APK 安装时触发的 dex2oat 编译,到 OAT 文件的结构、编译过滤器(compiler filter)的选择,再到系统 OTA 升级时的全局 dexopt——本文完整解析 Android DEX 优化流程。
一、从 DEX 到 OAT:编译流水线
1.1 整体流程
APK 中的 classes.dex(Dalvik Executable 字节码) |
AOSP 核心路径:
art/dex2oat/— dex2oat 编译器源码art/runtime/oat_file.h和art/runtime/oat_file.cc— OAT 文件结构art/runtime/oat_file_assistant.h— OAT 文件查找和验证art/libdexfile/dex/— DEX 文件解析库
1.2 dex2oat 的命令行
# 典型调用(由 installd 发起) |
1.2.1 参数详解
| 参数 | 含义 |
|---|---|
--dex-file |
输入 DEX 文件的路径(可以是 APK 文件也可以是独立的 .dex 文件) |
--oat-file |
输出 OAT 文件的路径 |
--compiler-filter |
编译过滤器,控制编译粒度 |
--instruction-set |
目标 CPU 指令集(arm/arm64/x86/x86_64) |
--app-image-file |
应用映像文件(用于存储 AOT 编译后的类对象) |
--classpath-dir |
Boot classpath 目录 |
--profile-file |
Profile 文件路径(用于 speed-profile 过滤器) |
1.3 dex2oat 的初始化与编译
// art/dex2oat/dex2oat.cc |
// art/dex2oat/dex2oat.cc |
1.4 dex2oat 的进程模型
dex2oat 不是以线程的方式在 system_server 中运行,而是由 installd 守护进程 fork 出独立进程。这样做的原因:
- 内存隔离:dex2oat 编译过程中会创建 ART Runtime,占用大量内存(首次编译可能需 100MB+),独立的进程在编译完成后可完全释放内存。
- 崩溃隔离:如果 dex2oat 因 OOM 或其他原因 crash,不会影响 system_server(AMS、PKMS 等)。
- 安全隔离:dex2oat 运行在
installd的 selinux domain 下,不与 system_server 共享权限。
二、OAT 文件结构
2.1 OAT 文件头
// art/runtime/oat.h |
magic 字段的作用:"oat\n" 这个 4 字节的魔数用于快速识别文件类型。如果文件格式不兼容,版本号会变更,ART 在加载时检测并拒绝加载。
key_value_store:一个字符串键值对列表,存储编译时的环境信息。如:
debuggable→ “true”/“false”classpath→ “/system/framework/…”compiler-filter→ “speed-profile”
2.2 OatDexFile——每个 DEX 文件的元数据
// art/runtime/oat_file.h |
OAT 文件中 DEX 文件偏移的格式是 0xPPPPNNNN,其中 PPPP 是 OAT 内部的偏移页,NNNN 标识具体位置。
2.3 OatClass——每个类的编译状态
// art/runtime/oat_file.h |
对于 kOatClassSomeCompiled 的类,每个方法有一个 32-bit 的偏移量(相对于 OAT 文件起始位置),指向其在 OAT 中的编译代码。运行时使用 lookup_table[method_idx] 快速定位方法代码。
2.4 OatQuickMethodHeader——每个方法的编译代码头
// art/runtime/oat_quick_method_header.h |
vmap_table 是 stack map(也称作 GC map),用于 GC 时解析栈帧——确定哪些寄存器/栈位置包含对象引用。
2.5 Boot Image 与 .art 文件
从 Android 5.0(ART 第一个正式版本)开始,系统预编译 boot classpath 中的类并存储在 /system/framework/arm64/boot.art 中。boot.art 是一个映像文件(image file),包含预初始化的类对象和它们的 AOT 代码:
boot.art = 类对象的内存快照 + AOT 编译代码 |
运行时,ART 直接 mmap boot.art 文件,避免了重新解析和编译 framework 类的大量工作。应用级别的 .art 文件(如 base.art)存储了应用自身的预加载类。
三、VDEX 文件格式
Android 8.0 引入了 VDEX 格式,包含:
- 未压缩的 DEX 文件:供运行时快速加载,无需从 APK zip 中解压
- Quickening 信息:预优化的字节码(如将虚方法调用的 method_idx 替换为 vtable index)
- 验证信息:DEX 字节码验证结果,运行时加载时可直接信任
VDEX 的设计目标:将 DEX 验证和部分优化工作从 APK 首次启动时(Runtime)前置到安装时(dex2oat),从而显著减少应用的冷启动时间。
// art/runtime/vdex_file.h |
3.1 Quickening 详解
Quickening 是面向 DEX 字节码的优化,不需要编译成机器码。具体优化包括:
- 虚方法调用优化:将
invoke-virtual {v0, v1}, method@idx中的 method_idx 替换为 vtable_index,运行时直接通过 vtable 跳转。 - 字段访问优化:将
iget/iput(实例字段访问)和sget/sput(静态字段访问)的 field_idx 替换为字节偏移量。 - 内联简单方法:getter/setter 等单行方法直接在字节码层面内联,避免方法调用开销。
Quickening 的优势:
- 编译速度极快:不需要生成目标机器的指令,只是修改 DEX 字节码的某些字段
- 文件体积增长小:比 AOT 编译的 .odex 小得多(约为原始 DEX 的 1.1-1.3x)
- 与 Profile-Guided JIT 互补:Quickening 优化了所有方法的基础路径,JIT 再进一步优化热点方法
四、Compilation Filter(编译过滤器)
dex2oat 支持多种编译过滤器,控制 AOT 编译的粒度。由 compiler-filter 参数指定:
| Filter | 行为 | 适用场景 |
|---|---|---|
| verify | 仅验证 DEX 字节码,不编译任何方法 | 开发调试阶段 |
| quicken | 优化字节码(Quickening),不编译为机器码 | 存储空间受限的设备 |
| speed-profile | 基于 Profile 指导编译:热方法 AOT,冷方法解释/JIT | 生产环境推荐(平衡性能与空间) |
| speed | 编译所有方法(full AOT) | 对性能极致要求的场景 |
| everything | 编译所有方法 + 运行时特殊路径(如 JNI stub) | 极少使用(空间开销巨大) |
| extract | 从 APK 中提取并验证 DEX,不做任何编译 | Android 11+ 新增,依赖 JIT |
| verify-none | 最低开销,跳过所有验证和编译 | 特殊场景(已预验证) |
// frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java |
// art/dex2oat/compiler_filter.h |
4.1 Profile-Guided Compilation 的工作原理
speed-profile 过滤器依赖应用运行时的 Profile 数据。工作流程:
- 运行时:ART JIT 编译热点方法,同时记录这些方法的 ID 到 profile 文件
- 空闲时:
BackgroundDexOptService触发dex2oat --compiler-filter=speed-profile --profile-file=... - dex2oat:只编译 profile 中记录的那些热方法(通常是全部方法的 10%-20%),其余方法保留为解释/JIT
这称为 Profile-Guided Optimization (PGO) 在 Android 上的应用。场景评估:启动时常用路径约几千个方法被编译,90%+ 的非关键路径方法保有解释/JIT 覆盖,OAT 文件大小缩减到全 AOT 的约 20%。
五、系统 OTA 升级时的 dexopt
当系统 OTA 升级完成后,framework 的 boot classpath 发生变化,需要重新编译所有应用的 OAT 文件。这个操作由 ota-dexopt 触发:
// frameworks/base/services/core/java/com/android/server/pm/BackgroundDexOptService.java |
系统升级后的 dexopt 路径:
ota-dexopt或bg-dexopt脚本被触发installd收到指令,遍历/data/app/下所有应用- 对每个应用调用
dex2oat,根据其当前的 compiler filter 重新编译 - 新生成的
.odex和.vdex文件覆盖旧的
5.1 OAT 文件验证与失效判断
ART 加载 OAT 文件时,通过 OatFileAssistant 进行多项检查:
- DEX checksum 匹配:当前 DEX 的 checksum 必须与 OAT 记录的一致
- Boot classpath 匹配:OAT 编译时的 boot.art checksum 必须与当前系统的匹配
- 版本号匹配:OAT 文件版本必须与 ART 支持的版本兼容
如果 boot classpath 在 OTA 后发生变化(framework 方法被修改),所有依赖旧 boot classpath 的 AOT 编译代码都失效——其中内联了旧版本的 framework 方法。这也是为什么 OTA 需要重新 dexopt。
六、安装时 dexopt vs OTA dexopt 的区别
| 维度 | 安装时 dexopt | OTA dexopt |
|---|---|---|
| 触发时机 | APK 安装完成时(PackageInstallerSession.commit) | 系统 OTA 升级后 |
| 范围 | 仅新安装/更新的应用 | 所有已安装应用 |
| 编译过滤器 | 新应用默认 speed-profile | 继承原有 filter |
| 执行环境 | 可与前台安装并行(后台线程) | 通常在充电且空闲时执行 |
| 实现类 | PackageManagerService.performDexopt | BackgroundDexOptService / otapreopt |
| 优先级 | 中等 | 低(不影响前台使用) |
6.1 Android 10+ 的 dexopt 延迟策略
Android 10 引入了 staged-dexopt——新安装应用不立即完全 dexopt,而是先做最小编译(verify 或 extract),待设备空闲/充电时再做完整 speed-profile 编译。这减少了安装等待时间。
6.2 Cloud Profiles
Android 10+ 引入了 Cloud Profiles(云配置文件),允许 Google Play 从云端下发应用的默认 profile。当用户首次安装应用时,即使没有本地 profile,也可以使用云端提供的 profile 指导 speed-profile 编译——这是基于”同型号设备上相似用户行为相似”的假设。
七、核心面试题
Q1:什么是 Quickening?它和 AOT 编译有什么区别?
Quickening 是在 DEX 字节码层面进行的优化,而 AOT 是将 DEX 字节码编译为机器码。Quickening 包括:将虚方法调用的 method_idx 替换为 vtable index、优化字段访问为偏移量查找、内联简单的 getter/setter 字节码。它不需要生成机器码,因此编译速度极快,文件体积增长小。AOT 编译则完全生成本地机器指令,执行速度快但编译时间长、输出文件大。Android 采用分层编译:默认 Quickening + JIT(热方法被 JIT 编译为机器码),通过 profile 记录热点,在后台 dexopt 时对这些热方法做 AOT。
Q2:VDEX 文件相比纯 OAT 有什么优势?为什么不直接全部放 OAT?
VDEX 包含未压缩的 DEX 文件和 quickening 信息,与 OAT 分离有几个好处:(1) 当系统 OTA 升级(boot classpath 变化)时,OAT 需要重编译(因为 AOT 代码内联了 Framework 方法),但 VDEX 中的 DEX 和 quickening 信息仍然有效,可以继续用来解释执行。(2) 未压缩的 DEX 可以直接 mmap,无需从 APK zip 中解压,减少了冷启动时的 I/O。(3) VDEX 分离也方便独立更新 DEX 验证结果。
Q3:dex2oat 在安装时被触发,如何确保不影响系统响应?
dex2oat 不是在 SystemServer 进程中直接执行,而是通过 installd 守护进程 fork 出独立的 dex2oat 进程执行。installd 以低 I/O 优先级(ionice)和低 CPU 优先级(nice)运行 dex2oat,而且 PackageManagerService 使用 BackgroundDexOptService 管理编译队列,确保同一时间只运行有限数量的 dex2oat 进程(通常 1-2 个),避免资源争用影响前台应用。
Q4:为什么 OTA 升级后需要对所有应用重新 dexopt?
AOT 编译过程中,dex2oat 会将 framework 方法内联(inline)到应用的编译代码中。当 OTA 升级修改了 framework(boot classpath),所有内联了旧 framework 方法的 AOT 代码都处于不一致状态(调用的可能已经不存在的指令偏移或已修改的方法体)。OAT 文件通过记录编译时的 boot.art checksum 来检测 boot classpath 是否发生变化——如果发生变化,OAT 文件被标记为无效,需要重新编译。
Q5:Cloud Profiles 是如何解决”冷启动无 Profile”问题的?
传统 Profile-Guided Compilation 需要用户先使用应用,JIT 收集热点方法后生成 profile。但新用户或新安装时没有 profile,只能使用较低级别的过滤(如 quicken)。Cloud Profiles 通过聚合大量设备上的匿名 profile 数据,提取出”最常见的执行路径”,形成云端默认 profile。当用户安装应用时,Google Play 同时下发这个 profile,dex2oat 使用它进行 speed-profile 编译——实现了”首次安装即可获得 profile 指导的编译效果”。
6.3 Android 12+ 的 ART 模块化
Android 12 引入了 Mainline 模块化的 ART(通过 Google Play System Updates)。这意味着:
- ART 运行时(包括 dex2oat)可以作为独立的 APEX 模块更新,而不用等待完整的系统 OTA
- 更新周期从原来的一年缩短到几个月甚至几周
- 新的 ODEX(.odex)文件格式与 ART 模块绑定的版本,确保一致性
APEX 模块更新后,系统会触发一次针对所有已安装应用的重新 dexopt(类似于 OTA dexopt 但范围更小——仅 ART 模块变更时触发)。
6.4 dexlayout 与 dex 重排
Android 10 引入了 dexlayout 工具,在编译时优化 DEX 文件中方法和类的排列顺序:
dexlayout 工作原理: |
这称为 Profile-Guided Layout Optimization。重排后的 DEX 文件写入 VDEX 中。
6.5 JIT 编译与 OAT 的关系
ART JIT 编译在运行时产生临时的机器码,存储在进程的 JIT code cache 中(非文件)。JIT 的数据流:
解释执行 → 热点检测(方法调用计数 + 循环回边计数) |
关键关系:JIT 是短期优化(运行时即时编译),OAT 是长期优化(持久化存储)。两者通过 profile 文件协同工作——JIT 标记哪些方法值得 AOT,dex2oat 在空闲时执行这些方法的 AOT 编译。
AOSP 核心路径参考:
art/dex2oat/dex2oat.cc— dex2oat 编译器入口art/dex2oat/compiler_filter.h— 编译过滤器定义art/runtime/oat_file.h/art/runtime/oat_file.cc— OAT 文件定义art/runtime/oat_header.h— OAT 文件头结构art/runtime/vdex_file.h— VDEX 文件定义art/runtime/oat_file_assistant.h— OAT 文件查找与验证art/libdexfile/dex/— DEX 文件解析库frameworks/base/services/core/java/com/android/server/pm/BackgroundDexOptService.javaframeworks/native/cmds/installd/— installd 守护进程
八、dex2oat 的编译流程深度剖析
8.1 DEX 文件验证阶段
dex2oat 的验证阶段检查 DEX 字节码的正确性:
验证内容: |
验证通过后生成 VDEX 文件中的 verification info。此后运行时加载时可以直接信任这些验证结果,跳过验证步骤——这是 VDEX 加速冷启动的核心。
8.2 Quickening 的具体优化内容
Quickening 是在 DEX 字节码层面进行的优化,不编译成机器码:
Quickening 优化项: |
Quickening 的编译速度极快(通常几毫秒到几十毫秒),因为它不涉及任何指令集相关的代码生成——只做索引替换。
8.3 AOT 编译的具体优化
AOT 编译产生目标机器的本地代码时,进行的优化包括:
AOT 优化项(与 LLVM/Clang 编译优化类似): |
8.4 OAT 文件的 mmap 映射
运行时加载 OAT 文件的关键是 mmap:
// art/runtime/oat_file.cc |
mmap 的核心优势:
- 零拷贝访问:文件内容直接映射到进程地址空间,无需 read() 系统调用
- 按需加载:操作系统按 page fault 加载必要的页(惰性加载)
- 页面共享:同一个 OAT 文件被多个进程 mmap 时,物理内存页共享(节省 RAM)
- 可执行权限:通过 PROT_EXEC 标记,mmap 的页面可直接执行 native 代码
8.5 运行时方法查找的完整路径
当 ART 需要执行一个 Java 方法时:
1. 获取 method_index(在 DEX 中的方法编号) |
AOSP 核心路径参考续:
art/runtime/oat_file.cc— OAT 文件加载与 mmapart/runtime/interpreter/interpreter.cc— ART 解释器art/compiler/optimizing/— AOT 编译优化器

