虚拟机 Dalvik 是 Android 操作系统的关键组件,负责执行应用程序直到 4.4 KitKat 版本。由丹·伯恩斯坦开发的基于寄存器的虚拟机取代了标准 JVM 的概念,使得可以针对内存有限的移动设备优化应用程序的启动。根据 Google, 2024 的数据,Dalvik 通过 JIT 编译确保应用程序兼容性,在运行时直接将 DEX 字节码转换为机器指令。
要点
Dalvik 是采用寄存器架构的虚拟机,专门为 Android 平台创建。开发始于 2005 年,由丹·伯恩斯坦的公司进行,2007 年该项目被 Google 收购。第一个商业版本的 Dalvik 随着 2008 年 Android 1.0 的发布而出现。
与标准 Java Virtual Machine(JVM)不同,Dalvik 不执行 Java 字节码。Java 编译器将源代码转换为 class 文件,然后 dx 工具将它们转换为 Dalvik Executable(DEX)格式。这种格式比 class 文件更紧凑:一个 10 MB 的 class 格式应用程序在 DEX 中约占 6–7 MB。
丹·伯恩斯坦将 Dalvik 编写为面向资源有限的操作系统的项目。这个名字取自冰岛村庄达尔维克。Google 之所以选择 Dalvik 而不是 JVM,是因为许可限制以及需要针对带有 ARM 架构的移动处理器进行深度优化。该系统迅速流行起来:到 2012 年,超过 5 亿台 Android 设备运行在 Dalvik 上。
每个 Android 应用程序都在单独的进程中运行,拥有自己的 Dalvik VM 实例。这确保了操作系统层面的数据隔离和恶意代码防护。这种方法将虚拟化的优势与 Linux 沙箱相结合——一个应用程序中的恶意软件无法影响相邻的进程。
Dalvik 的 寄存器架构从根本上不同于 JVM 的堆栈架构。Dalvik 不是在栈顶进行操作,而是操作寄存器——虚拟机内部的虚拟单元。每条指令包含操作数寄存器的地址,这减少了每次操作的指令数量。
JVM 的堆栈机器使用 push、pop 和 add 等指令——将两个数字相加需要三条指令。Dalvik 用一条带三个寄存器的 add-int 指令解决同样的任务。根据 Android Open Source Project 的数据,DEX 的寄存器架构平均将字节码体积比堆栈式 class 格式减少 30%。
DEX 文件(Dalvik Executable)包含应用程序所有类的压缩表示。文件头包括校验和、各节大小和偏移量。主要部分是字符串池、类型池、方法原型池、字段池以及字节码本身。一个 DEX 文件中最多可以存储 65536 个方法(该限制通过 Android 5.0 中引入的 multi-dex 解除)。
要将 class 文件转换为 DEX,需要使用 dx 工具,它包含在 Android SDK Build Tools 中。命令示例:dx --dex --output=classes.dex myapp.jar。现代项目使用 D8——dx 的继任者,具有改进的优化和对 Java 8+ 功能的支持。
# 使用 dx 将 JAR 转换为 DEX
dx --dex --output=classes.dex myapp.jar
# 通过 D8 的现代版本
d8 --lib android.jar --output dex/ myapp.jar
Zygote 进程是 Dalvik 架构中最重要的元素。系统启动时,Zygote 加载 Android SDK 的所有类,打开共享库并创建预加载资源池。当用户打开应用程序时,系统复制(fork)Zygote 进程,创建已经拥有现成框架的 Dalvik VM 新实例。这将应用程序启动时间从约 2–3 秒缩短到 300–500 毫秒。
JIT(Just-In-Time) 是一种在应用程序运行期间直接将字节码编译为机器指令的技术。在 Dalvik 中,JIT 编译器分析正在执行的 DEX 代码,识别经常使用(hot)的方法,并将它们编译为 CPU 的原生代码。
在早期 Android 版本中选择 JIT 而不是完整的 Ahead-Of-Time(AOT)编译是有意为之。移动设备的 闪存有限(4–16 GB)——预先编译所有应用程序会占用大量空间。此外,早期设备中的 ROM 存储器比 RAM 慢,读取预编译代码可能会降低性能。
当应用程序启动时,Dalvik 开始解释 DEX 字节码。一个特殊的分析器跟踪哪些方法被最频繁调用。超过阈值(通常约 200 次调用)后,JIT 编译器将方法转换为机器代码并缓存在内存中。后续调用使用已编译的版本,无需重新编译。
// JIT 将编译的 hot 方法示例
public class Calculator {
public int sumArray(int[] arr) {
int total = 0;
for (int i = 0; i < arr.length; i++) {
total += arr[i];
}
return total;
}
}
根据 Google I/O 2013 的数据,在 Android 2.2 Froyo 中引入 JIT 使应用程序执行平均比纯解释快 2–5 倍。然而,JIT 在首次启动时会增加延迟:应用程序需要 3 到 10 秒来预热和编译 hot 方法。预热后,性能稳定在接近原生代码的水平。
Dalvik 与 JVM 在几个基本参数上有所不同。第一是架构:JVM 基于堆栈,Dalvik 基于寄存器。第二是字节码格式:JVM 使用 class 文件,Dalvik 使用 DEX。第三是内存管理:Dalvik 针对移动设备的 有限内存进行了优化。
两种方法都有各自的优势。基于堆栈的 JVM 存储指令占用的空间更少——每条指令更短,因为操作数隐式取自堆栈。基于寄存器的 Dalvik 每次操作执行的指令更少,这节省了处理器时间并降低了功耗。对于使用电池供电的移动设备来说,这至关重要。
| 参数 | Dalvik | JVM |
|---|---|---|
| 架构 | 寄存器 | 堆栈 |
| 字节码 | DEX | class |
| 编译 | JIT(Android 2.2+) | JIT / AOT |
| 优化 | 低功耗 | 高兼容性 |
| 隔离 | 通过 Linux 进程 | 通过 ClassLoader |
选择 Dalvik 而不是 JVM 也是由 许可决定的。Oracle 拥有 Java SE 和 JVM 的权利,Google 力求避免支付许可费。创建具有替代字节码格式的自有 VM 使 Android 能够独立于 Oracle 发展。这一争端演变成 Oracle 与 Google 之间长达多年的诉讼(2010–2021),最终以 Google 胜诉告终。
DEX(Dalvik Executable) 是包含 Android 应用程序编译代码的二进制格式。每个 DEX 文件都以头(header)开始,后面是各节:字符串常量(string_ids)、类型(type_ids)、方法原型(proto_ids)、字段(field_ids)、方法(method_ids)、类定义(class_defs)和数据区域(data)。
dx 工具将 Java class 文件转换为一个或多个 DEX 文件。工作算法包括常量去重——相同的字符串或类型只保存一次并按索引引用。这显著减小了最终体积。在现代项目中,dx 已被 D8 取代(出现在 Android Studio 3.1 中),它运行快 2–3 倍并支持 Java 8 desugaring。
// 通过 dexdump 反编译 DEX 字节码的示例
// 源代码:return a + b;
@Ldalvik/annotation/Code;
registers: 3
add-int v0, v1, v2
return v0
DEX 格式对 65536 个方法的限制(16 位索引的限制)成为大型应用程序的严重问题。解决方案出现在 Android 5.0:multi-dex 支持允许应用程序包含多个 DEX 文件。主要的 classes.dex 包含入口点,而额外的 classes2.dex、classes3.dex 等等包含其余代码。multi-dex 配置通过 build.gradle 中的 multiDexEnabled true 行启用。
Dalvik 中的垃圾回收实现为带标记清除(mark-and-sweep)的分代(generational)收集器。内存分为两个主要区域:用于对象的 Heap(堆)和用于基本类型与引用的 Stack(栈)。当 Heap 填满时,Dalvik 暂停所有线程(STW — Stop-The-World),标记可达对象并释放不可达对象。
在 Android 2.2 之前,Dalvik 使用单线程收集器,暂停时间长达 100–200 毫秒。在 Android 2.3 Gingerbread 中出现了并发收集器,将典型暂停缩短到 5–10 毫秒。而在 Android 4.0 Ice Cream Sandwich 中添加了具有部分(增量)清理的收集器——Concurrent Mark and Sweep(CMS)。
Dalvik 应用程序的典型问题是通过对 Activity 的静态引用造成的内存泄漏。如果静态字段保存对 Context 或 View 的引用,垃圾收集器即使在屏幕关闭后也无法释放 Activity。Eclipse MAT 和 LeakCanary 等工具有助于发现此类泄漏:它们分析 Heap 转储并显示持有对象的引用链。
// 通过静态引用造成内存泄漏的示例
public class Utils {
private static Context context;
public static void init(Context ctx) {
context = ctx; // 在 finish() 之后仍持有 Activity
}
}
尽管取得了成功,Dalvik 仍有一系列缺点。JIT 编译需要预热时间——应用程序运行的最初几秒较慢。此外,JIT 在编译时会消耗处理器电量,缩短了续航时间。随着移动设备性能的提高和内置内存容量的增加,对 JIT 的需求降低了。
在 Android 4.4 KitKat 中,Google 将 ART(Android Runtime)作为 Dalvik 的实验性替代品推出。从 Android 5.0 Lollipop 开始,ART 成为唯一的运行时环境。主要区别是 AOT 编译:不是在运行时编译,而是在安装时将所有应用程序编译为机器代码。这消除了预热延迟并改善了能效。
从 Dalvik 到 ART 的过渡对开发人员是透明的:两种运行时都执行相同的 DEX 字节码。为 Dalvik 构建的应用程序可以在 ART 上无需重新编译即可运行——system_server 在安装时将它们编译为原生代码。例外情况是使用反射访问 Dalvik VM 内部成员的代码:由于内部架构的变化,此类代码可能在 ART 上出错。
常见问题
Dalvik 是一个中间程序,在手机上运行 Android 应用程序。它获取应用程序的代码并将其转换为处理器可以理解的指令,在用户使用过程中直接完成。
Dalvik 使用寄存器架构和 DEX 格式,而 JVM 使用堆栈架构和 class 格式。Dalvik 针对内存和处理器有限的移动设备进行了优化,而 JVM 面向台式计算机和服务器设计。
ART 通过预先 AOT 编译提供更高的性能——应用程序在安装时编译一次,而不是每次启动时编译。与 Dalvik 的 JIT 方法相比,这加快了运行速度并节省了电池电量。
可以,ART 完全向后兼容 Dalvik 的 DEX 字节码。安装时,ART 将旧的 DEX 文件编译为原生代码。例外情况是使用反射访问 Dalvik 内部机制的应用程序。
DEX(Dalvik Executable)是包含 Android 应用程序压缩字节码的可执行文件格式。如果应用程序包含超过 65536 个方法,一个 APK 中可以有多个 DEX 文件(multi-dex)。
结论
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。