ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android运行时编译产物解析:dex/odex/oat/vdex/art

Android运行时编译产物解析:dex/odex/oat/vdex/art 每次装完一个 App你去/data/app下翻一圈大概率能看到一堆后缀看起来像“乱码”的文件base.apk之外还有base.odex、base.vdex、base.art在部分 Android 版本上甚至还能翻到base.oat。别小看这几个文件它们背后是一整套“从 Java 代码到机器码”的编译运行体系。今天想把 dex、odex、oat、vdex、art 这五类文件串起来一次讲清楚它们是什么、怎么产生的、谁负责生成、丢了会怎样以及如何在实际开发和日常分析里用好它们。这篇文章适合 Android 应用开发者、ROM 定制玩家、负责系统性能优化的同学以及所有对“Android 到底怎么跑起来”这件事有好奇心的人。1. 先认人五类文件到底是什么1.1 开发者唯一亲手制造的dex先说 dex。.dex全称是 Dalvik Executable它由 Android SDK 里的构建工具d8/dx把 Java/Kotlin 编译出的.class文件合并转换而来。你在 Android Studio 里点一次 Build工程里的源码会先被 javac/kotlinc 编译成字节码再被 d8 压缩进一个或多个classes.dex最后和资源一起打包成 APK。可以说dex 是所有 Android 应用运行逻辑的载体。dex 是一种紧凑的字节码格式专门为移动端设计指令集短、常量池紧凑、字符串和方法引用大量去重所以同一个 App 的 dex 体积通常只有对应 jar 包的六成左右。它不面向某个具体 CPU 架构也就是说这份字节码是“跨平台”的真正把它翻译成 ARM 指令还是 x86 指令取决于设备上的运行时环境。这里要提醒一句三星有个桌面模式叫 DeX和本文说的 dex 字节码完全是两码事网上搜“三星 DeX”时会看到一堆扩展坞、外接显示器的内容别搞混。1.2 odex 与 oat两代运行时对优化文件的接力odex是 Optimized Dalvik Executable出现在 Android 2.x 到 4.x 的 Dalvik 时代。它的作用是在应用安装或系统首次启动时系统会提前对 dex 做一次“预优化”——包括字节码验证、指令重排、常量池修正等然后把优化结果单独存成.odex文件放在/data/dalvik-cache/里。这样运行时加载类时就不用每次重复验证减少了开机和冷启动的耗时。但它本质上是给 Dalvik 解释器用的“半成品优化”并没有真正生成机器码执行效率仍然有限。oat则是 ART 时代Android 5.0 起的第一代产物。它把 dex 里的热点方法提前编译成当前设备 CPU 架构的本地机器码同时又把原始 dex 完整嵌入在文件里。oat 文件本质上是一个 ELFExecutable and Linkable Format文件你可以拿readelf -h xxx.oat直接看它的文件头结构里既有.text存放编译后的机器码也有.rodata存放 dex 和元数据。这样做的好处是运行时可以把 dex 和本地代码放在同一个可执行文件里一次 mmap 全部映射进内存加载效率非常高。代价也很明显全量 AOT 编译极耗 CPU 和存储空间。1.3 vdex 和 art8.0 之后的新“基建”Android 8.0 引入了vdexVerified Dex它的核心作用是承载 dex 文件的“验证信息”。我们知道ART 在加载 dex 之前要做方法验证、指令快速化quickening等工作Android 7.0 之前的方案是把这些结果和 dex 放在一起到了 8.0Google 把验证结果和 dex 本体拆开vdex 文件里既包含未修改的原始 dex 副本也包含验证信息和 quickened 数据这样 dex2oat 在后续优化时可以直接复用不用每次重新验证。vdex 的 magic number 是vdex开头你直接用十六进制编辑器打开就能看到。art这个后缀有点容易误导人它并不保存“机器码”而是 ART 运行时镜像文件。里面存的是类对象布局、字符串哈希表、预解析的字段偏移等运行时数据结构。为啥需要它因为应用启动时如果要一个个类去解析、分配、初始化开销非常大有了 art 镜像文件系统可以直接把这份“已就绪的类状态”映射到内存里类加载和字段访问就快得多。所以你会发现base.art文件体积通常不大但它对冷启动速度的影响相当明显。1.4 五类文件速查表文件全称出现版本核心内容典型存放位置dexDalvik ExecutableAndroid 1.0 至今开发者编译出的字节码APK 内classes.dexodexOptimized Dalvik ExecutableAndroid 2.x ~ 4.x预验证/预优化的 dex/data/dalvik-cache/oatART Compiled ExecutableAndroid 5.0 ~ 7.xELF 本地机器码 内嵌 dex/data/app/pkg/oat/vdexVerified DexAndroid 8.0 至今dex 副本 验证信息/data/app/pkg/oat/下同目录artART ImageAndroid 5.0 至今类布局、字符串池等运行时镜像/data/app/pkg/oat/这张表解决了“每个文件是啥”的问题但更关键的问题是为什么同一个 APK 在 Android 4.x 和 Android 14 上生成的产物文件不一样这就要从 Android 运行时的进化史说起。2. 为什么会有这些文件一条运行时进化主线2.1 Dalvik 时代解释执行和 odex 的妥协早期的 Android 用的是 Dalvik 虚拟机核心执行方式是“解释执行 JIT 即时编译”。直觉上解释执行最省事但缺点是效率低一个 Java 方法被调用 100 次就要被解释 100 次热循环里的性能损失很扎眼。odex 在那个时代解决的是“启动加载慢”的问题不是执行慢的问题。APK 安装后系统调用dexopt工具生成 odex把 dex 里已验证的方法信息、类查表结构提前整理好加载时就能少干活。但要注意odex 并不是本地机器码它仍然是字节码Dalvik 每次执行时还是得靠 JIT 把“反复执行的热方法”编译成 native code一旦进程重启JIT 编译的结果就没了下次冷启动又得重新来。这是 Dalvik 时代 App 冷启动慢、滑动掉帧频发的根本原因之一。2.2 ART 时代oat 的全量 AOT 与代价Android 5.0 将 ART 设为默认运行时核心思路从“边跑边编译”变成“安装时就编译”。dex2oat 这个系统进程会在应用安装、系统升级或 OTA 后读取 APK 里的 dex把它编译成机器码产物就是 oat 文件。机器码直接放在.text段运行时不需要再经过解释器或 JIT方法调用直接跳转到本地代码执行效率肉眼可见地提高。但这个方案不是没有代价。全量 AOT 编译非常耗时一个大型游戏项目在慢速设备上可能占用 dex2oat 进程一分钟以上安装时间大幅拉长oat 文件也比纯 dex 大很多16GB 存储的设备很容易被撑爆更头痛的是系统升级后部分 oat 依赖底层代码变动一旦失效Android 就得在首次开机时对所有应用重新做一次 dexopt于是你会在升级完系统后看到“正在优化应用”的进度条转半天动都不动。我当时做 ROM 兼容性验证时经常被用户的优化进度条问题弄得焦头烂额。Google 显然也意识到了这个体验缺陷所以 Android 7.0 开始转向混合编译方案。2.3 混合编译JIT AOT profileart 文件的回归Android 7.0 引入了“JIT 预编译 运行时 profile 引导”的混合模式也是从这一版开始/data/app下能看到带base.art后缀的文件。这套机制可以拆成三步看第一应用安装后系统只做轻量验证不主动全量编译安装速度大幅回落。第二应用运行过程中ART 自带的 JIT 会持续统计哪些方法被高频调用生成 profile 文件通常在/data/misc/profiles/cur/0/包名/下。第三当设备处于空闲、充电状态时后台的 dex2oat 会根据 profile 文件只编译那些热点方法生成新的 oat 文件同时把类运行时的布局数据写入 art 文件。这个设计非常聪明它把“热点方法”的选择从“全都要”变成“只编最需要的”既控制住了安装时间又保证了高频方法能拿到机器码性能。你会发现同一个应用使用一段时间后启动速度往往比刚安装时更快这正说明后台的 profile 引导编译起作用了。base.art文件就是在这一阶段产生的里面保存的是 JIT 运行时和后台编译时积累下来的“类图照片”下次冷启动直接照着加载省去重复解析。2.4 boot image系统框架的预编译镜像聊完应用再说系统。Android 进程启动时系统框架里几百个framework.jar里的类不可能等进程跑起来了再一个个解析否则任何 App 打开都要卡上好几秒。于是 ROM 在烧录或首次开机时会把 boot classpath 里的所有 dex 统一编成一份引导镜像通常出现在/system/framework/arm64/目录下命名就是boot.art、boot.oat、boot.vdex。每个应用进程的 zygote fork 出来后ART 会先把这个 boot image 映射进内存框架类几乎都是“现成的”字段偏移、方法入口已经在 art 镜像里算好不需要重复初始化。你可以做个小实验把某个应用从后台划掉再冷启动速度快得惊人很大程度上是因为 boot image 已经把框架层“预热”完了。如果系统没有这个镜像基本等同于每个应用都要从零开始加载框架那种停滞感你绝对不想体验。理解完这条进化线“为什么有这些文件”已经不再是谜题。但纸上谈兵不够我们得真正在设备上操作一把看看这个机制是怎么被触发的。3. 亲手控制编译过程dex2oat 与 compiler-filter3.1 用 adb 触发一次 dexopt很多做性能测试和 ROM 调优的人都需要手动触发应用的重新编译用来对比不同编译策略下的启动耗时。方法很简单先找一个已经安装的应用执行adb shell dumpsys package dexopt | grep -A 8 com.example.app输出里能看到类似这样的信息[com.example.app] path: /data/app/com.example.app-xxx/base.apk status: speed-profile compiler-filter: speed-profile ...status字段告诉你这个应用当前处于哪种编译状态compiler-filter则说明上次 dex2oat 用的是什么过滤级别。接下来我们强制触发一次全量 AOT 编译命令是adb shell cmd package compile -m speed -f com.example.app这条命令会立刻运行 dex2oat 进程你可以开另一个终端窗口观察adb shell ps -A | grep dex2oat编译完成后再去看应用的 oat 目录adb shell ls -l /data/app/com.example.app-xxx/oat/arm64/正常能看到三个文件base.art base.odex base.vdex这里有个容易混淆的点8.0 之后oat 目录里默认不叫base.oat而是继续用base.odex这个名字但内容实际是 ART 架构下的优化后 ELF 文件和 Dalvik 时代的 odex 完全是两代产品。很多新手对着base.odex一头雾水看了本文应该就能分清安卓 8.0 之后你在 oat 目录下看到的.odex后缀本质是 dex2oat 的输出文件格式和早期 odex 不是一回事。3.2 compiler-filter 参数对照与选择dex2oat 的核心参数是compiler-filter官方定义了 5 个主流级别。我把它们整理成一张表filter编译内容适用场景verify只做验证不编译任何方法快速启动、电量敏感quicken轻量字节码快速化不生成机器码需要低延迟安装的应用speed-profile只编译 profile 里的热点方法常规应用默认值最推荐speed编译所有未抛异常的方法核心应用、测试对比everything尽量编译所有方法场景极少编译时间最长日常开发中绝大多数设备默认是speed-profile。你可以用adb shell getprop dalvik.vm.dex2oat-filter查看当前设备的默认 filter如果是厂商定制 ROM这个值可能被改过。什么时候需要手动调整如果你在测应用冷启动性能希望拿到“纯 AOT”的数据就切到speed测一轮如果想模拟“刚安装未优化”的状态就切到verify。注意切 filter 后设备空闲时的后台 dex2oat 可能还会按自己的策略再次编译实测时最好把目标应用放到一个可控的测试环境里或者关掉自动更新避免结果被后台任务干扰。3.3 观察产物编译前后文件差异为了直观理解编译过程可以在编译前记录一下目录清单编译后再对比一次。编译前应用全新安装时可能只有base.vdex还没有base.art触发全量 AOT 编译后base.odex体积会明显增大base.art也会出现。用 adb pull 把编译前的文件拉下来对比adb pull /data/app/com.example.app-xxx/oat/arm64/base.vdex ./然后编译后再拉一遍base.odex在宿主机上用readelf -h查看它readelf -h base.odex你能看到 ELF 头里的Type字段以及 Machine 字段指向 ARM/ARM64 或者 x86。这就是“本地机器码已经躺在 oat/odex 文件里”的铁证。如果同一个 APK 分别安装在 ARM 设备上生成的文件只对应 ARM 指令集换一个 x86 模拟器重新安装生成的文件又会变成 x86 版本这也解释了为什么/data/app下的优化产物不能随意从一个设备拷贝到另一个设备上用。4. 逆向分析视角从 odex/oat/vdex 里提取 dex4.1 jadx 直开 oat 的原理做应用分析的同学一定好奇过为什么有的 APK 从应用商店下载下来用 jadx 打开却看不到完整的 classes.dex因为这些分发包在签名、加固、抽取时做了手脚而如果从/data/app下把base.apk拉出来却往往能得到一份“能直接反编译”的包因为系统在安装时已经默认帮你把 dex 校验并还原过一遍dex 以明文形式放在 vdex 里。更直接的做法是直接用 jadx 打开 oat 文件。前面说过oat 里内嵌了完整的 dex 副本所以jadx base.odex很多时候能直接列出所有类和方法。为什么能这么做因为 dex2oat 在设计上为了保证系统兼容和运行时回退故意把原始 dex“原封不动”存了一份在 oat 文件里机器码只是新增的附加数据。也就是说oat 文件 原生 dex 编译后的本地代码 元信息反编译工具只需要找到 ELF 段里的 dex 块就能按常规套路处理。4.2 oatdump 解析 oat/vdex 结构如果你要做更底层的分析比如确认某个方法是否真的被编译成了机器码oatdump是 AOSP 自带的瑞士军刀。它在 host 端通常不能直接跑需要 Android 源码环境编译或者从带调试符号的 ROM 里提取对应版本的可执行文件把目标 oat 拉到同一环境里执行oatdump --oat-filebase.odex --outputoat_dump.txt输出文件里会分段列出Dex File Header内嵌 dex 的文件头信息包括 checksum、method_ids_size 等。OatDexFile记录 dex 在 ELF 中的偏移、class 个数、编译方法列表。OatMethodOffsets每个方法的本地代码入口偏移。这些信息对分析“方法有没有被 AOT 编译”“编译后的代码在哪里”特别有用。比如你发现一个热方法在oat_dump.txt里没有对应的本地代码入口那说明它当时没被识别为热点运行时大概率靠解释器或 JIT 顶着这就是启动性能上不去的一个潜在原因。4.3 vdexExtractor 提取 vdex 中的 dex在 Android 8.0 及以上版本想要纯 dex 文件最稳的路径是从 vdex 里提取。Google 为了后续 dex2oat 复用vdex 里保存了一份和原始 APK 里几乎一致的 dex 副本。开源社区有个工具叫vdexExtractor专门干这件事。大致流程是# 从设备里拉取 vdex adb pull /data/app/com.example.app-xxx/oat/arm64/base.vdex ./ # 校验并提取 ./vdexExtractor -i base.vdex -o ./out执行后out目录下会得到class_classes.dex之类的文件。之后就能交给 baksmali 还原成 smali 代码baksmali d out/classes.dex -o smali_out再配合 jadx 查看 Java 层结构或者直接在 smali 层看指令细节。必须补充一句vdex 里的 dex 有时会缺失部分 method 的源代码信息因为加固壳会用抽取指令的方式破坏原始 dex 的应用层完整性但系统层编译过的 vdex 相对干净得多对付常规分析基本够用。4.4 关于合法性的一点提醒做分析和逆向不是为了去破解别人辛苦开发的成果。这里分享的提取方法主要用于三种合法场景一是排查自己开发的 App 在发布到市场后安装到设备上的文件状态是否符合预期二是学习经典的 dex/oat 结构加深对系统运行时的理解三是在获得授权的情况下做安全合规检测。未经许可提取第三方应用的代码并二次分发或商用属于明确的侵权行为切勿去踩那条线。5. 高频问题与踩坑速查5.1 常见问题表问题原因解决方案删了 base.odexApp 打不开ART 启动时依赖优化后的 oat/odex 结构恢复原文件或重装应用触发重新 dexoptbase.art 丢失缓存被清理或人为删除通常无大碍App 会重新生成仅首次启动变慢dex2oat 占满 CPU 导致设备卡顿安装/系统升级后全量重新编译切到speed-profile或等待空闲期后台编译vdex 无法直接打开文件包含 dex 验证数据混合结构用vdexExtractor提取后反编译oat 反编译结果和 APK 不一致OAT 内嵌 dex 可能与 APK 原 dex 有出入对比 vdex 提取结果优先用 vdex 里的 dex厂商 ROM 首次开机很久对所有系统应用执行 dexopt属正常现象优化策略可编辑 build.prop 调整5.2 最易踩的两个坑文件缺失与编译风暴第一个坑是误删优化产物。一些“系统清理”App 会把/data/dalvik-cache或/data/app/xxx/oat下面的文件当成缓存清掉结果就是应用启动时 ART 找不到已编译的本地代码只能退回解释执行或临时 JIT冷启动慢到让人抓狂。严重的时候dex 验证信息缺失会导致应用直接崩溃。我的建议是对于系统关键应用永远不要手动删这些文件宁可多占点存储也别换来一堆兼容性问题。第二个坑更隐蔽叫“编译风暴”。你在一台低配设备上批量安装大量应用或者系统刚从旧版 OTA 升级上来dex2oat 会疯狂吃 CPU 编译所有应用设备会变得又烫又卡。排查方式很简单adb shell ps -A | grep dex2oat如果能看到多个 dex2oat 线程在轮转说明系统正在执行后台优化任务。这时最好把应用安装操作拆开或直接等待设备进入空闲状态。在测试环境中还可以通过adb shell device_config put runtime_native_boot_dex2oat_threads 2之类的方式限制并发编译线程但这类参数因版本而异动手前先查一下当前 Android 版本的 device_config 是否支持。5.3 结合文件状态判断应用的编译策略日常做系统优化时我习惯用一套“三步法”快速了解某个应用的编译状态第一步用dumpsys package dexopt看 filter 状态第二步去 oat 目录看文件体积和是否存在 art 文件第三步看设备空闲期是否还有 dex2oat 在后台跑。这套流程的最终目的是确认“应用当前是解释执行为主还是机器码为主”进而决定优化方向。举个例子某个应用冷启动到了 3.5 秒但你发现它的 filter 状态是verify说明 dex2oat 根本没编译任何方法。这时可以手动切到speed-profile运行几天后再测大概率能挤出几百毫秒的启动收益。反过来如果你发现应用已经是speed但启动还是慢那就得往代码层面找原因了线程调度、IO、资源加载这些都可能比编译策略影响更大。一点个人心得我做 Android 系统优化这些年和这五个文件打过太多交道。有一次做 OTA 灰度新版固件把默认 filter 从speed-profile调成了speed结果升级后用户首次开机要等十几分钟后台 dex2oat 把设备拖到几乎没法操作差评如潮。后面我们改回speed-profile并且增加了一个“分应用分批编译”的调度才把这个体验问题压下去。从那以后我看任何 ROM 调优方案都会先问一句dex2oat 的过滤器和触发时机是什么这几个文件背后的编译决策看似只是技术细节实际上直接影响着用户每天第一次点亮屏幕时的等待时间。希望这篇梳理能够帮你建立起一个清晰的心智模型下次再面对“为什么这个应用安装后跑得慢”“为什么这些文件不能乱删”这类问题时能有一个明确的判断坐标。
返回列表