针对dex文件,做android开发的应该都见过,没见过也听说过,至少听说过65536吧,本篇文章就带大家认识以下dex。

什么是dex文件

大家知道JVM 是 JAVA 虚拟机,用来运行 JAVA 字节码程序。Dalvik 是 Google 设计的用于 Android平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在Android 4.4推出。这块内容可参看我之前的一篇文章《关于Android虚拟机的那些事儿》

Android 程序一般使用 Java 语言开发,但是 Dalvik 虚拟机并不支持直接执行 JAVA 字节码,而是会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。所以说Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。

了解了 Dex 文件以后,对日常开发中遇到一些问题能有更深的理解。如:APK 的瘦身、热修复、插件化、应用加固、Android 逆向工程、64 K 方法数限制。

dex文件有什么好处

有同学可能有疑问了,Android系统其实是基于Java语言上开发(此处暂且不谈kotlin),而Java源码编译后生成的是class字节码文件,该文件也是存储了Java源码的相关信息,Android系统为何不采用使用class文件而使用dex文件呢?

android采用dex代替class文件,且不说与甲骨文的那点小过节,更多的是针对移动设备所做出的努力。class文件中的数据规范分明,每个class代表一个类,结构清晰,但它对于移动设备而言还是有以下弊端:

  • class文件中包含各种数据如常量池、field等,而一个应用中有成百乃至更多的类,使用字节码文件存储类信息,内存占用过大,不适合移动端;
  • class文件是堆栈的加栈模式,加载速度慢;
  • 文件IO操作多,类查找慢;因为每个class文件中只存储了一个Java源文件信息。

这三个是最明显的弊端,因此不适合移动设备的加载使用,而Dex是对众多 .class 文件进行了整合并且做了很多优化,整体统一,空间占用小,易加载(dex基于寄存器),在不脱离java的束缚下是移动端最好的应对方案。

class和dex内部区别

dex文件是怎么生成的

java 代码转化为 dex 文件的流程如图所示:

java代码转化为dex文件的流程示意图

例如有一个Hello.java文件,则通过两步即可生成dex文件:

第一步: 通过 JDK 的 javac 工具,将 .java文件转化为.class二进制文件。

javac Hello

第二步: 通过 SDK 中的 dx工具,将 .class文件进行翻译、重构、解释、压缩等操作,生成 .dex 文件。

dx --dex --output=Hello.dex Hello.class

dex文件的具体格式

就像 MP3,MP4,JPG,PNG 文件一样,Dex 文件也有它自己的格式,只有遵守了这些格式,才能被 Android 运行时环境正确识别。

Dex 文件整体布局如下图所示:

Dex文件整体布局

  • 文件头:header记录了dex文件信息及所有字段大致的分布;
  • 索引区:分别记录了字符串、类型、方法原型、域、方法的索引,这部分指定了dex文件中所有不同类型数据存储的位置,数据最终存储于“数据区”;
  • 数据区:此块可分成普通数据区和链接数据区,后者听起来较为陌生,总所周知Android中常有一些动态链接库so的引用,而链接数据区就是对这个的指向。

dex文件头

文件头区域决定了该怎样来读取这个文件。具体的格式如下表(在文件中排列的顺序就是下面表格中的顺序):

Dex文件头区域

Dex文件头二进制一览:

Dex文件头二进制一览

65536问题

好多同学都知道android可执行文件“.dex”中的Java方法数引用不能超过65536个,否则就会报com.android.dex.DexIndexOverflowException:method IDnotin[0,0xffff]:65536的错误。但是为什么是65536呢?

这个问题是由于dex文件格式限制,一个dex文件中的method个数采用使用原生类型short来索引文件的方法,也就是2个字节共16位,最多表达65536个method,field/class个数也均有此限制,对于dex文件,则是将工程所需要全部class文件合并压缩到一个dex文件期间,也就是Android打包的dex过程中,单个dex文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为66536。

因此如果工程代码量比较大,方法数若超过65536个则必须采用dex分包形式处理。

对于为什么采用short来作为索引范围,我猜当时工程师考量的问题还是和移动端硬件和内存比较吃紧有关,纯属个人想法。

odex、oat、vdex

dex文件是DVM虚拟机可执行的文件,但是真正在app运行的时候虚拟机并不是执行的dex文件。虚拟机运行程序之前需要对dex文件做进一步优化,进而降低内存占用,提高执行效率。

odex

全称:optimized dex; 即优化过的dex。

Android5.0之前APP在安装时会进行验证和优化,为了校验代码合法性及优化代码执行速度,验证和优化后,会产生odex文件,运行Apk的时候,直接加载odex,避免重复验证和优化,加快了Apk的响应时间。

oat

oat是ART虚拟机运行的文件,是ELF格式二进制文件,包含dex和编译的本地机器指令,oat文件包含dex文件,因此比odex文件占用空间更大。

Android L( 5.0 ) 引入Android Runtime (ART),ART 使用设备自带的 dex2oat 工具来编译应用,提高启动速度,dex2oat默认会把classes.dex翻译成本地机器指令,生成ELF格式的oat文件,ART加载OAT文件后不需要经过处理就可以直接运行,它在编译时就从字节码装换成机器码了,因此运行速度更快。

不过android5.0之后oat文件还是以.odex后缀结尾,但是已经不是android5.0之前的文件格式,而是ELF格式封装的本地机器码。可以认为oat在dex上加了一层壳,可以从oat里提取出dex。

vdex

Android O ( 8.0 ) 引入了vdex,目的是为了降低dex2oat时间。

vdex 文件有助于提升软件更新的性能和用户体验。vdex 文件会存储包含验证程序依赖项且经过预验证的 dex 文件,以便 ART 在应用更新期间无需再次解压和验证 dex 文件。从而优化了启动速度。

vdex 目的不是为了提升性能,而是为了避免不必要的验证Dex 文件合法性的过程,例如首次安装时进行dex2oat时会校验Dex 文件各个section的合法性,这时候使用的compiler filter 为了照顾安装速度等方面,并没有采用全量编译,当app启动运行一段时间后,收集了足够多的jit 热点方法信息,Android会在后台重新进行dex2oat, 将热点方法编译成机器代码,这时候就不用再重复做验证Dex文件的过程了。这点有点类似机器学习。

最后

本文仅仅是对dex整体结构做了一个初步的认识,至于dex内部的深度刨析,作者不做系统开发,因此没有做过深研究。对dex有一个基础看法,这将为以后的类加载、热修复、插件化等技术栈提升有很大的帮助。如果大家有任何的建议或意见,欢迎指出。喜欢的点个赞!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐