DEX(Dalvik可执行文件)是一种字节码格式,Java和Kotlin编写的Android应用程序的源代码会被编译成这种格式。DEX文件由Dalvik虚拟机(截至Android 4.4)或Android运行时(ART,从Android 5.0开始)执行。根据Android开源项目,2026的数据,与标准JVM字节码相比,DEX格式平均提供30%更紧凑的代码表示。
要点
DEX(Dalvik可执行文件)是一种专为Android移动设备设计的字节码格式。与标准Java字节码(.class文件)不同,DEX针对有限资源进行了优化:内存占用更少、体积更小、类加载更快。
Java或Kotlin的源代码由javac/kotlinc编译为标准.class文件(Java字节码)。然后d8工具(或之前的dx)将.class文件转换为一个或多个DEX文件。这种转换不仅仅是重新打包——d8执行优化:合并常量池,将指令重写为寄存器架构,以及删除重复数据。
DEX使用寄存器架构(与JVM的栈式架构不同)。每个方法有固定数量的寄存器(最多65536个)。DEX指令更短——平均2字节,而JVM为1-4字节。这使得代码更紧凑:一个典型的应用程序从10-15 MB的.class减少到4-6 MB的.dex。
DEX文件具有严格定义的二进制结构。每个文件以头部开始,包含多个通过偏移量相互引用的部分。。
| 部分 | 用途 |
|---|---|
| header | 头部:魔数、校验和、签名、各部分的大小和偏移量 |
| string_ids | 字符串表:类、方法、字段的名称 |
| type_ids | 类型:对类型字符串标识符的引用 |
| proto_ids | 方法原型:返回类型和参数 |
| field_ids | 类字段:类、类型、名称 |
| method_ids | 方法:类、原型、名称 |
| class_defs | 类定义:标志、超类、接口、数据偏移量 |
| data | 实际数据:方法代码、注解、调试信息 |
DEX的魔数是——`dex\n035\0`(版本035)。其他版本:036、037、038(适用于Android 8.0+)。大小为0x70字节的头部包含SHA-1校验和以及所有部分的偏移量。头部验证——虚拟机加载DEX时的第一步。
string_ids、type_ids、proto_ids、field_ids、method_ids——是索引表。方法代码中不存储完整名称,而使用4字节索引。这是关键优化:如果一个类被引用100次,其名称在string_ids中只存储一次。dex2oat在ART编译期间进一步优化这些表。
将源代码转换为DEX的过程由多个阶段组成。现代工具链使用D8编译器,该编译器于2018年随Android Gradle Plugin 3.2取代了DX。
javac(用于Java)或kotlinc(用于Kotlin)将源代码编译为.class文件。每个类——Java字节码中单独的.class文件。在这个阶段,执行类型检查、桥接方法生成和常量嵌入。
D8接收所有.class文件并将其转换为DEX字节码。D8执行多项优化:删除未使用的方法参数,合并来自不同.class的常量池到一个全局DEX池,将JVM栈指令转换为Dalvik寄存器指令。
// Kotlin源代码
data class User(
val name: String,
val email: String
)
fun greet(user: User): String {
return "Hello, ${user.name}!"
}
经过D8编译后,这段代码变成紧凑的DEX指令:const-string用于加载字符串,iget-object用于访问对象字段,invoke-virtual用于调用StringBuilder.append。
D8比DX快2-3倍,生成更紧凑的DEX(小5-10%),并且更好地优化Kotlin特定结构(内联函数、lambda)。DX自2018年起被标记为已弃用,并从Android Gradle Plugin 8.0中移除。
DEX代码在Android中的执行经历了两个阶段:原始的Dalvik虚拟机(Android 2.2-4.4)和Android运行时ART(Android 5.0+)。编译方法的差异是根本性的。
Dalvik使用即时(JIT)编译:DEX字节码被解释,频繁调用的方法在运行时被编译为本机代码。优点——安装快速。缺点——启动较慢和JIT持续消耗CPU。
ART(Android运行时)在安装应用程序时通过dex2oat将DEX编译为本机代码。这是一种预先(AOT)编译方法:安装时间更长,但启动更快且能耗更低。从Android 7.0开始,ART使用混合方法——AOT + JIT + 配置文件引导优化。
dex2oat工具在安装或更新应用程序时运行。它将DEX编译为适用于设备架构的带有本机代码的ELF文件。结果——/data/dalvik-cache/目录中的.oat和.art文件。Google不断改进dex2oat:在Android 14中增加了对折叠设备的优化。
每个DEX文件65536个方法的限制——来自Dalvik架构的遗留问题。DEX头部中的method_ids字段占用4字节,最多提供2^16 = 65536个唯一引用。使用Google Play Services、Firebase和其他SDK的现代应用程序很容易超过此限制。
Multidex是一种将代码分割为多个DEX文件的机制。主classes.dex包含入口点(Application类、主要Activity),其余——classes2.dex、classes3.dex等。启动时,来自额外DEX的类通过DexClassLoader加载。
// build.gradle.kts — 启用multidex
android {
defaultConfig {
multiDexEnabled = true
}
}
// 具有multidex支持的Application类
class MyApp : Application() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
}
}
在应用程序启动阶段加载额外的DEX可能会导致Android 5.0及更低版本的设备上出现ANR(应用无响应)。建议——仅在必要时使用multidex,并尽量减少依赖项以避免超过限制。
DEX优化——构建发布版Android应用程序的标准阶段。R8和ProGuard工具减小DEX的大小,混淆代码并删除未使用的类。
R8——ProGuard的继承者,自2019年起内置于Android Gradle Plugin。R8在一次遍历中完成缩减、混淆和优化,而ProGuard需要两个阶段:ProGuard → D8。ProGuard仍然受支持,但Google建议新项目使用R8。
R8删除未使用的类、方法和字段,将它们重命名为短名称(a、b、c),嵌入内联函数并丢弃死代码。结果——DEX在不损失功能的情况下减小20-40%。
R8的配置在proguard-rules.pro文件中定义。开发者可以指定哪些类不能重命名(例如,对于反射或Gson序列化)。Firebase和其他SDK在其依赖项中提供自己的规则。
DEX可以反编译回Java代码。这是Android应用程序安全的关键问题:没有混淆,代码会被恢复到接近原始的水平。
JADX——最流行的DEX到Java反编译器。它恢复类名、方法、字段和大部分逻辑。apktool将DEX反编译为smali代码(Dalvik汇编器)——接近原始指令的低级表示。Bytecode Viewer在一个界面中结合多个反编译器。
R8/ProGuard混淆——第一道防线:类和方法的名称变得不可读。DexGuard——具有附加方法的商业工具:字符串加密、完整性检查、防篡改。控制流级别的混淆(O-LLVM)改变代码的结构,保留其功能,但使分析变得非常困难。
常见问题
DEX使用寄存器架构而不是JVM的栈式架构,格式更紧凑(小30%),将所有.class文件合并到一个具有统一常量池的文件中,并使用16位索引而不是8位索引。
Smali——是DEX字节码的汇编器。每个DEX指令都有smali格式的文本表示。baksmali工具将DEX转换为smali(反汇编),而smali将smali汇编回DEX。
Gradle的countMethods任务或dex-method-counts插件显示每个DEX文件中的方法数量。adb shell命令配合dumpsys也会显示已安装应用程序的已加载DEX统计信息。
是的,在Android 8.0及更低版本的设备上,多个DEX会减慢应用程序启动速度,因为每个额外的文件都是单独加载的。在Android 8.0+的ART上,由于dex2oat编译为单个.oat文件,差异很小。
可以,存在一些项目,如dexplorer和与Android兼容的JVM实现,它们可以在Android之外执行DEX字节码。然而,大多数DEX文件使用Android API,这使得它们不适合在普通JVM上运行。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。