Skip to content

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 快速解释层
JITart/runtime/jit热点方法(计数器+调用阈值)现编现用;tiered(Android 8+)先轻编后重编
AOToat/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(动态加载/插件)。三个硬约束:

  1. 双亲委托不可绕过(loadClass 模板方法固定顺序);类相等 = 同名 + 同 ClassLoader 实例。
  • 多 ClassLoader 事故簿:插件与宿主各带一份同名库 → ClassCastException 却类名相同——先查 loader 身份再怀疑字节码。
  1. sealed version(Android 9+ 强化):dex 被「降级替换」会直接抛 SecurityException——热修复类方案被平台钉死的根本原因。
  2. 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 当一等公民维护。

参考链接 ​

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处