Appearance
ART:dex 结构、混合执行与 profile 引导编译
本页导读:同一份 dex,在 ART 里可以走解释器、JIT、AOT 三条路,谁快谁慢由 profile 说话。本页按「字节码长什么样 → 三种执行引擎 → 什么时候 dex2oat → 类如何加载链接初始化」的主线展开,并解释 boot image、启动类预加载与 ClassLoader 分层的真实关系。前置:架构篇 init/Zygote 页。
一、dex 速览:为移动端省出来的容器
dex(Dalvik Executable)是为「受限内存 + 解释执行」设计的字节码容器:magic
dex\n035~039(Android 5~8 时代定型,新版在 ART 内按兼容性读),头部之后是一串 ID 表(string_ids/type_ids/proto_ids/field_ids/method_ids/class_defs)+ 数据区 + map_list。与 JVM class 的两大差异:基于寄存器而非操作数栈;类型引用是索引表而非逐处内联字符串——这让 verifier 可以线性扫表校验。
65536 方法引用上限是索引位宽限制(method_ids 用 u2);multidex 在 API 21+ 由运行时原生支持,但 D8/R8 的脱糖与 shrink 才是当代正解。
D8 负责 dex 化与 desugaring(java.time/lambda/默认方法重写为兼容类),R8 负责 shrink/optimize/obfuscate——「dex 是构建产物」的现代口径。
bash
# 看一个 APK 的 dex 结构(build-tools)
d8 --version; dexdump classes.dex | head -40 # 头部与各 ID 表尺寸
apkanalyzer dex packages list app.apk # 包/类/方法计数(R8 keep 效果核验)
| dex 段 | 装什么 | 工程含义 |
|---|---|---|
| header | magic/checksum/各表偏移 | 兼容性检查点 |
| string/type/proto ids | 名称与签名去重表 | R8 改名/裁剪直接缩表 |
| field/method ids | 成员引用 | 64K 上限的当事表 |
| class_defs + data | 类定义与字节码 | 优化后写进 vdex |
| map_list | 段目录 | 解析入口 |
dex 与 class 的哲学差:class 面向动态链接(符号字符串到处飞),dex 面向表驱动(一切皆索引)——这决定了 ART 校验快、优化时能整段重写(进 vdex)。二、三种执行引擎:解释器、JIT、AOT
ART 是混合编译器:同一方法在不同时刻走不同引擎。
| 引擎 | 位置 | 特点 |
|---|---|---|
| 解释器 | art/runtime/interp | 零启动成本;现代版本另有 nterp 快速解释层 |
| JIT | art/runtime/jit | 热点方法(计数器+调用阈值)现编现用;tiered(Android 8+)先轻编后重编 |
| AOT | oat/vdex(dex2oat 产物) | 装机/后台预编译,启动即机器码,但占存储且难全覆盖 |
对照 JVM 一句:JVM 分层编译追求峰值吞吐、启动慢可忍;ART 追求「启动即顺 + 稳态不拖」,profile/AOT 权重远高于纯 JIT。 时间线(面试高频):
- Android 5.0 ART 首发:装机即全量 AOT,升级后卡顿被吐槽到爆;
- Android 7.0:转向 JIT 优先 + profile 收集,AOT 延后;
- Android 8/9:profile 引导编译成熟——按热点清单
speed-profile编译,兼顾启动与体积; - Android 12+:dex2oat 相关逻辑收进 ART APEX 内的 ART Service(模块可独立更新);
- 现在装机体验:装完先用解释器/JIT 跑,profile 攒热后闲时后台再编,热路径 AOT 化。
JIT 细节两笔(面试深水区):
- 计数器驱动:方法调用/回边计数超阈值进编译队列;tiered 下先轻量编译、持续热再重编回填;
- code cache 有上限,满了整组 reset 重计——「长任务后半程才变顺」的机制解释。
三、dex2oat 的时机与产物
```text
触发源:
├ 安装/更新:PackageManager 触发 ART Service → 按当前 profile(或 baseline)做 speed-profile
├ 首次开机/OTA 后:boot image 重建 + 系统分区应用后台 dexopt(充电+待机时机)
├ 日常后台:ART Service 定期 idle+charge 任务,读新 profile 增量编译
└ 手动调试:adb shell cmd package bg-dexopt-jobstatus / art 模块工具(以版本为准)
产物(/data 与 /oat 目录族):
.oat 机器码与元数据 | .vdex 校验+优化后的 dex | .art 堆快照(boot image/app image)编译过滤档(compilation filter,取值以 AOSP art/compiler 文档为准):verify(只校验)→ speed-profile(只编热点,默认正解)→ speed(全编,boot 关键模块用);历史上的 quicken 已退役。
bash
# 查某包编译产物与后台任务(路径/子命令随版本,以设备为准)
adb shell ls -l /data/user/0/com.example/oat/arm64/
adb shell cmd jobscheduler list-jobs | grep -i dexopt
- **Baseline Profile**:库/应用**自带**的热点清单(随 APK 分发,`assets/dexopt/`),让首次安装就能编对热路径——现在主流 SDK 与 Jetpack 都发,值得自己用 Macrobenchmark 生成后随包发布。
- profile 落盘位置在 `/data/misc/profiles/` 族(cur/参考两态,细节以设备为准);profman(ART 内组件)负责合并与重放。
profile 质量陷阱:样本只覆盖「测试机操作路径」会漏真实热路径;多机型聚合 + 随版本刷新才可运营(Baseline Profile 工具链已支持远程产物合并,以 AndroidX 文档为准)。
## 四、boot image、预加载与「启动类引用」
```text
Zygote 启动资产装载顺序(概念图):
① mmap boot.art/boot.oat(核心类与 AOT 码共享)
② 读 preloaded-classes 批量解析/初始化类
③ preloadResources(常用 drawable/字符串)
④ 进入 fork 服务循环;App 进程 fork 即指针级复制这些资产系统侧把「几乎所有进程都要的类」固化成共享资产,省 RAM 也省启动:
boot image(boot.oat/boot.art,现随 APEX 分布于
/apex/*/javalib族):核心库 AOT 码 + 预初始化的类与对象堆快照,Zygote fork 时直接 mmap 共享;预加载:Zygote 启动读
/system/etc/preloaded-classes批量Class.forName,把常用类提前解析/初始化进 Zygote 堆——所有 App 进程 fork 即得,这是「Android 冷启动快」的第一功臣;app image(配合 GC record-replay):后台记录一次「App 启动期的分配轨迹」,dex2oat 生成把启动对象固化进 app image 的产物,下次启动直接 mmap 恢复——冷启动优化的系统级形态;
boot image 与 app image 的区别一句话:前者全设备共享(核心库),后者一 App 一份(启动对象快照)。
「启动类引用」口径:profile/预加载共同刻画的「启动关键类集合」,Baseline Profile 的 startup 段就是它的产品化表达(术语映射以 ART 文档为准)。
冷启动度量口径:TTID(Displayed)与 TTFL(首帧可交互);app image 优化打 TTID,业务链路优化打 TTFL,两本账别混。
bash
# 看某包的编译状态(filter/时间/baseline 命中)
adb shell dumpsys package dexopt com.example | head -30五、ClassLoader 分层与 sealed version
java
// frameworks/base/core/java/dalvik/system/BaseDexClassLoader.java(findClass 骨架大意)
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
for (DexPathElement element : dexElements) { // 按 classpath 顺序
Class<?> c = element.dexFile.lookupClass(name); // 在已打开的 dex 容器里找
if (c != null) { c.setClassLoader(this); return c; }
}
throw new ClassNotFoundException(name); // 注意:先父后己,双亲委托
}分层图:BootClassLoader(boot classpath)→ 系统 PathClassLoader(framework)→ 应用 PathClassLoader(APK dex)→ 自定义 DexClassLoader(动态加载/插件)。三个硬约束:
- 双亲委托不可绕过(
loadClass模板方法固定顺序);类相等 = 同名 + 同 ClassLoader 实例。
- 多 ClassLoader 事故簿:插件与宿主各带一份同名库 → ClassCastException 却类名相同——先查 loader 身份再怀疑字节码。
- sealed version(Android 9+ 强化):dex 被「降级替换」会直接抛
SecurityException——热修复类方案被平台钉死的根本原因。 - ClassLoaderContext/命名空间:动态加载必须申报上下文,系统校验合法祖先(禁止「野路子」链式加载),native 库也按 linker namespace 隔离。
动态加载的最小正确姿势(热修复玩法已被 sealed version 判死刑,合法场景只剩插件/资源包):
kotlin
val loader = DexClassLoader(
apkPath, nativeLibDir, null,
context.classLoader // parent 需合法:ClassLoaderContext 会校验祖先
)
val c = loader.loadClass("com.plugin.Entry")六、类状态机与启动类引用表
一个类在 ART 里有明确状态:Loaded → Resolved → Verified → Initialized,对应懒加载三件套:
解析(Resolution):符号引用(类型/方法/字段)用到才查表解析,App Image 可预解析省运行时;
校验(Verification):软失败设计——verify 不过只把方法标「运行时再验」,不拦进程;
初始化(Initialization):首次主动使用触发
<clinit>,JVM 语义一致(父先于子、接口默认方法另有规则)。校验时机分 boot 校验(开机对 bootclasspath)与运行时软失败,应用 dex 的 install 校验结果就记在 vdex 里。
方法入口桩也分阶段:解析桩 → 跳桩 → AOT/JIT 入口;类状态推进的可见形式就是桩替换。
启动性能视角:主线程被拖慢的常见真凶是启动期触发重 <clinit> 链(一个巨型单例的静态块拖出一串类解析)——Baseline Profile 的 startup 段 + 后台线程预热的组合拳打的正是这里。
七、常见误区问答
Q1:JIT 让 Android 比 JVM 慢? 启动快、峰值到得慢是设计取舍;profile 引导 AOT 后热路径是机器码,稳态差距在长循环极端场景才可见。移动端看的是 P95 体验不是峰值吞吐。
Q2:multidex 现在还要开吗? minSdk 21+ 后 multidex 仅作为历史遗留存在,D8 默认处理引用切分;新工程开 legacy multidex 只会拖慢启动。
Q3:adb shell cmd package compile 强制编译有用吗? 调优阶段可以验证 speed/speed-profile 的启动差值(需特权),线上要靠 Baseline Profile,不能指望用户机器闲时任务。
Q4:为什么我的反射在 16 上找不到 hidden 字段? hidden API 名单制(非 SDK 接口限制)随版本收紧,反射黑魔法要用 @Keep/官方等价 API 替换;黑魔法类问题一律按「随时会坏」设计降级。
Q5:.vdex/.oat/.art 删了会怎样? 那是编译产物:删掉只退回解释器+JIT,功能无损,系统后台会重新 dexopt——当「提速偏方」清它们是玄学。
Q6:ProfileInstaller 是什么? Baseline Profile 工具链在 Manifest 自动注册的组件,安装后把随包 profile 交给 profman 入库——那是官方优化在起跑。
版本注记:基于 Android 16 / API 36;dex 035~039 兼容读;Android 7 JIT 时代、8 tiered+profile、9 sealed version/hidden API、12 ART Service 收编、13~16 持续演进 boot image 与 app image 策略(具体路径以设备为准)。
本页小结:ART = 解释器兜底 + JIT 追热点 + profile 引导 AOT 的混合引擎;dex2oat 的哲学从「装完就全编」进化为「让数据(profile/baseline)决定编什么」;boot image 与预加载撑起共享启动资产,ClassLoader 分层 + sealed version 钉死加载秩序与安全边界——应用侧的对应动作只有一个:把 Baseline Profile 当一等公民维护。
参考链接
- https://source.android.com/docs/runtime
- https://developer.android.com/topic/performance
- https://cs.android.com/android/platform/art
- https://cs.android.com/android/platform/art/+/main:runtime/jit
- https://cs.android.com/android/platform/frameworks/base/+/main:core/java/dalvik/system/BaseDexClassLoader.java
- 站内:内存与 GC、进程模型、架构篇 init 与 Zygote