Java到机器码:Android虚拟机的性能进化
从Java 源代码到机器码(ELF/OAT的演进过程中,核心在于 Android 虚拟机(从 Dalvik 到 ART)如何不断平衡“编译速度”、“运行性能”和“存储空间”的关系。
以下是整个链路的详细拆解:
1. 编译期:从 Java 到 Dex (打包阶段)
这是开发者在电脑上完成的过程。
- Java 源代码 (.java) ->字节码 (.class):
使用javac将代码编译成标准 Java 字节码。
- 字节码 (.class) -> Dalvik 字节码 (.dex):
使用d8(以前是dx)工具。这一步是 Android 的关键优化:
- 寄存器指令集:标准 Java 字节码是基于“栈”的,而 Dex 是基于“寄存器”的(更符合物理 CPU 的运行逻辑)。
- 常量池合并:将所有.class文件中的重复常量合并,极大地减小了 APK 体积。
- 打包成 .apk:将classes.dex、资源文件、动态库(.so)打包成压缩包并签名。
2. 运行期前奏:从 Dex 到 Odex (安装/预编译阶段)
当 APK 到达手机后,系统不能直接运行“压缩包”里的代码,需要经过dexopt(优化)或dex2oat(预编译)。
Odex 的本质
Odex(Optimized Dex) 的含义在 Android 演进中发生了变化:
- Dalvik 时代:Odex 只是对 Dex 进行了简单的校验和指令替换,本质还是字节码。
- ART 时代 (Android 5.0+):Odex 变成了一个包含本地机器码(Native Code)的容器。
3. 核心转换:从 Dex 到 ELF/Oat (编译原理)
现在的 Android 系统使用ART (Android Runtime)。它在安装或后台充电时,会调用dex2oat工具。
编译流程:
- Frontend(前端):解析classes.dex,构建代码控制流图(CFG)。
- Optimization(优化):
- 内联(Inlining):把你之前想要强制修改的inline_max_code_units应用在这里。将小函数直接嵌入调用处,减少函数调用开销。
- 指令重排、死代码删除。
Backend(后端):针对手机 CPU 架构(如 ARM64),将优化后的中间代码转换成特定的机器指令。
生成 ELF 文件:
最终生成的文件(通常后缀仍叫.odex或.oat)实际上遵循标准的ELF (Executable and Linkable Format)格式。
- 代码段 (.text):里面是直接给 CPU 运行的二进制指令。
- 数据段:存储代码依赖的常量和类信息。
- Vdex 文件:为了加快下次编译速度,系统还会生成一个.vdex,存储原始 Dex 信息。
4. 运行态:执行过程
当用户点击图标启动 App 时:
- 加载:系统通过mmap将 ELF 文件(Odex)映射到内存中。
- 符号链接:解决代码中对系统库(如framework.jar)的调用地址。
- 执行:
- AOT (Speed 模式):CPU 直接跳转到 ELF 文件中的代码段开始运行机器码。
- JIT (混合模式):如果没预先编译,虚拟机先解释执行 Dex 字节码,同时监测“热点代码”,在后台偷偷把经常运行的部分编译成机器码。
总结:文件演进关系表
阶段 | 文件格式 | 核心内容 | 谁来读取 |
开发环境 | .java | 程序员写的代码 | 编译器 |
安装包内 | .dex | Dalvik 字节码 | 虚拟机 |
优化产物 | .vdex | 原始 Dex + 校验码 | 编译器 (加速下次编译) |
最终形态 | .odex(ELF) | ARM 机器指令 | 手机 CPU (直接运行) |
