[译]Dex文件格式

Dex文件格式

原文链接:https://blog.bugsnag.com/dex-and-d8/

你是否好奇Android应用是如何编译和到爆陈apk的呢?本文将以微型Dex文件的实际例子深入Dalvik
可执行格式

Dex文件是什么?

Dex文件包含最终由Android运行时执行的代码。每个APK都有唯一的class.dex,它包含了应用的所有类或方法。本质上,代码库中任何使用的ActivityObjectFragment都将被转换成Dex文件中的字节,并可作为ANdroid应用程序运行。

理解Dex文件的结构是有用的,因为所有这些引用在你的应用程序中占用了很多空间。 使用许多第三方库可能会使您的APK大小增加兆字节,或更糟的是,导致臭名昭着的64k方法大小限制。 当然,也许会有一天,Dex文件的知识可以帮助您追踪应用程序中的意外行为

Dexing处理

Android工程中的所有Java源文件首先编译成.class文件,它包含字节码指令。在传统的Java应用中,这些指令会在JVM中执行。然而,Android应用执行在Android运行环境,它使用不兼容的操作码,因此需要额外的Dexing步骤,其中.class被转换成单个.dex文件。

由于大多数移动设备在内存,处理能力和电池寿命方面都收到严重限制,ART为JVM提供了卓越的行么。实现这个目标的一个关键特征是ART可执行AOT和JIT编译。这可以避免JIT的一些运行时的开销,同时还可以在应用程序进行配置时随时间改进性能

如何产生一个Dex文件

实际例子有助于更好的理解。让我们生成一个只包含一个Application的最小APK,因为这样可以让我们理解文件格式,而不会被典型应用程序中存在的数千种方法所淹没。

使用Hexfiend
以十六进制查看我们的Dex文件,因为Dex使用一些不寻常的数据格式来节省空间。我们已经隐藏了所有null字节,所以上面截屏中的空白字符实际上代表了00

一个Dex文件的格式

480字节的Dex文件完整结构以十六进制显示,下面显示为UTF-8格式。当解析为UTF-8时,某些部分可以立刻识别,比如我们代码中顶一个的单个BugsnagApp类,其他则不会:

6465780A 30333800 7A44CBBB FB4AE841 0286C06A 8DF19000
3C5DE024 D07326A2 E0010000 70000000 78563412 00000000
00000000 64010000 05000000 70000000 03000000 84000000
01000000 90000000 00000000 00000000 02000000 9C000000
01000000 AC000000 14010000 CC000000 E4000000 EC000000
07010000 2C010000 2F010000 01000000 02000000 03000000
03000000 02000000 00000000 00000000 00000000 01000000
00000000 01000000 01000000 00000000 00000000 FFFFFFFF
00000000 57010000 00000000 01000100 01000000 00000000
04000000 70100000 00000E00 063C696E 69743E00 194C616E
64726F69 642F6170 702F4170 706C6963 6174696F 6E3B0023
4C636F6D 2F627567 736E6167 2F646578 6578616D 706C652F
42756773 6E616741 70703B00 01560026 7E7E4438 7B226D69
6E2D6170 69223A32 362C2276 65727369 6F6E223A 2276302E
312E3134 227D0000 00010001 818004CC 01000000 0A000000
00000000 01000000 00000000 01000000 05000000 70000000
02000000 03000000 84000000 03000000 01000000 90000000
05000000 02000000 9C000000 06000000 01000000 AC000000
01200000 01000000 CC000000 02200000 05000000 E4000000
00200000 01000000 57010000 00100000 01000000 64010000

dex
038zDÀª˚JËAÜ¿jçÒê<]‡$–s&¢‡pxV4dpÑêú¨ÉÏ,/ˇˇˇˇWp<init>Landroid/app/Application;
#Lcom/bugsnag/dexexample/BugsnagApp;
V&~~D8{"min-api":26,"version":"v0.1.14"}ÅÄÃ
pÑêú¨ Ã ‰ Wd

解析Dex文件头

在很高的层次上,Dex文件可以被认为是两个独立的部分。 包含元数据的文件头,以及包含大部分数据的主体。 文件头结构图如下所示。

让我们顺序遍历头部中的每个项目。

Dex FILE MAGIC
许多文件格式以固定的字节序列开始,唯一标识用于操作它们的应用程序,Dex也不例外。

6465780A 30333800
dex
038

我们看到开头8个字节必须含有’dex’,以及版本号 - 当targetSdkVersion是API 26时,当前是38。

你可能还注意到,第4个字节表面是换行符,第8个字节是空。Android Frameowrk用它来检查文件损坏 - 如果这些确切的序列不存在应当拒绝安装APK。

CHECKSUM

7A44CBBB

下一个值是checksum,它是通过对整个文件的内容应用一个函数来计算的,并排除校验和之前的任何字节。如果文件中的某个字节在磁盘下载或存储过程中被损坏,则计算的校验和将不匹配,Android Framework将拒绝安装APK。

SHA1 SIGNATURE

FB4AE841 0286C06A 8DF19000 3C5DE024 D07326A2

头部还包含文件的SHA-1哈希(不包含任何前面的字节)。这用于唯一标示Dex文件,在Multidex场景中可能有用。

FILE SIZE

E0010000
480

以字节为单位计算文件大小,也用于读取Dex文件时的验证。

HEADER SIZE

70000000
112

文件的头部大小是112字节长。

因此,我们现在可以关注header_item中的所有剩余字段。

ENDIAN CONSTANT

78563412

Dex文件支持大端和小端编码。这个值等于REVERSE_ENDIAN_CONSTANT,表示这个特定的Dex文件是以小端编码的,这是默认行为。

IDS AND OFFSETS

文件头部余下的值定义了方法,字符串和其他项的标识符的位置和偏移量。

00000000 00000000 64010000 05000000
70000000 03000000 84000000 01000000
90000000 00000000 00000000 02000000
9C000000 01000000 AC000000 14010000
CC000000

这些值汇总在下表中,其中大小等于数组长度,偏移量是从可以找到该信息的文件起始处的字节数。

Type Size Offset
link_size 0 0
map_off N/A 356
string_ids 5 112
types_ids 3 132
proto_ids 1 144
field_ids 0 0

methods_ids|2|156|
|class_defs|1|172|
|data|276|204|

值得注意的是link_sizefield_ids都是0,因为我们的应用没有静态连接任何库或包含任何字段。data部分中的map_off结构在很大程度上复制了这些信息,对Dex文件解析来说会更容易一些。

Map列表

map_list是数据体的一部分,他包含于文件头类似的信息。

有了这些知识,我们可以使用偏移量来解析实际的信息,并确定我们的Dex文件的编码。

字符串

瞎掰结束——让我们看点实际例子。让我们来看看string_ids结构指向什么。

E4000000 EC000000 07010000 2C010000 2F010000
228,     236,     263,     300,     303

数组包含5个整型偏移量,指向数据部分。

<init>
Landroid/app/Application;
Lcom/bugsnag/dexexample/BugsnagApp;
V
~~D8{"min-api":26,"version":"v0.1.14"}

如果我们以UTF-8格式检索这些值,我们会看到几个Java符号,这些符号对于之前使用过JNI的任何人来说都很熟悉,还有一些JSON指出D8创建了Dex文件。 所有这些ID,偏移和多头部在这一点上可能看起来有点无用。为什么不直接在头部中对字符串值进行编码呢?

这背后的一些推理是这些字符串是从Dex文件中的多个点引用的。为每个ID提供一个ID可以防止信息的重复,并减少整体文件的大小,简化解析,因为ID将始终是一个固定的长度,意味着只有在需要的时候访问值。

类型
01000000 02000000 03000000
1, 2, 3

我们的Dex文件定义了3种Java类型。这里的每个值都是前一个string_id数组的索引 - 因此我们可以确定文件中的类型如下所示:

Landroid/app/Application;
Lcom/bugsnag/dexexample/BugsnagApp;
V

TypeDescriptor语法可能看起来有些陌生,但是L只是指完整的类名,而Vvoid的类型。我们的类型包括我们自定义的BugsnagApp类,以及Android框架中的Application类。

PROTOTYPES

03000000 02000000 00000000
3,       2,       0
"V",     V

方法原型由方法的返回类型信息和参数个数组成。proto_id部分使用索引来检索类型信息和一个偏移量,这里是不起作用的,因为我们的方法没有使用任何参数。

METHOD

方法部分也使用索引。每个方法都从字符串表中查找定义的类ID,方法原型和方法的名称。

00000000 00000000 01000000 00000000
0,  0,   0,       1,  0,   0

Landroid/app/Application "V" <init>
Lcom/bugsnag/dexexample/BugsnagApp; "V" <init>

我们的Dex文件中唯一的方法与BugsnagApp的构造函数有关 - 这正是我们所期望的。

CLASS DEFS

本节包含类型,继承层次结构,访问元数据以及其他类元数据,如注释和源文件索引。

01000000 01000000 00000000 00000000 FFFFFFFF 00000000 57010000 00000000
1,       1,       0,       0,       NO_INDEX,0,       343,     0

这是一个publicLcom/bugsnag/dexexample/BugsnagApp,其继承自Landroid/app/Application,其类数据是从字节343开始存储的。public访问修饰符由位域确定。 我们来查看这个类的数据。

CLASS DATA

BugsnagApp类数据的前4个字节定义了静态和实例字段的数量,以及任何直接或虚拟方法。

00 00 01 00
0, 0, 1, 0,

01 81 80 04 CC 01 00 00 00
1,          460

这个类只定义了一个直接的方法。 它的ID为1,对应于Lcom/ bugsnag/ dexexample/ BugsnagApp; “V”?<init>,以及一个460的代码数据偏移量。如果我们的方法是abstract的或nativve的,则不会有代码数据偏移量。

如果我们的类定义了字段和其他信息,更多的数据将被编码在本节中。顺便说一句,如果方法ID是一个大于65,536的值,我们会遇到臭名昭着的64k方法限制

CODE STRUCTURE

我们现在来分析类中定义的构造函数方法,它的偏移量为460结构如下

0100 0000 5701 0000 0010, 0000 01000000 64010000
1,   0,   343, 0,   16,   0    1,       64,1

这对应于寄存器大小为1,0个传入参数,343个传出参数,以及存储调试信息的偏移量16。

然而,最重要的部分是最后几个字节。 我们有一个指令列表大小为1,这意味着我们的方法已经编译为一个操作码:64010000

Dalvik字节码表建议64对应于寄存器上的sget-byte操作,使用字段引用索引1.这似乎与我们预期的单一BugsnagApp字段将为我们的应用程序创建相匹配 - 但深入Dalvik是另一个故事!

新Android编译器 - D8

我们还没有涉及到编译过程,但是我们最小的Dex文件是使用D8创建的,这个新编译器将在Android Studio 3.1中默认推出。 它在整体文件大小和构建速度方面提供了性能优势,所以我们来测试这些优化。

基准D8性能

我们用Android Studio 3.0.1构建一个应用程序。我们将添加Kotlin支持和一个导航抽屉,其他选项会使用默认值,生成签名的APK,并使用APK分析器查看。

我们可以解压缩APK文件检索class.dex,方法是unzip app-release.apk -d app,然后以字节为单位测量文件大小:stat -f%z app/classes.dex

更好更快更小更强

测量项 DX D8
未压缩文件大小(Mb) 4.23 3.73
类数 2790 2790
方法数 22038 22038
方法引用总计 28653 28653

使用D8编译时,我们的Dex文件大约是以前的大小的88%。您的效果可能会有所不同,因为这是一个非常简单的示例项目。还有一件有趣的事情要注意,使用D8,我们似乎失去了以下两个方法引用:

android.view.View#isInEditMode
java.lang.Class#desiredAssertionStatus

这些在运行时没有看到,所以可能是一个优化。如果你知道为什么消失了,请联系我们。

为什么压缩能得到更好的app

启用压缩和混淆处理是可为应用程序做的最重要的事情,现在您已经是Dex格式的专家了,您可以考虑下列原因。

首先,使用Proguard去除未使用的Java类将会减小APK的大小,因为生成的Dex文件减小了未使用的类定义以及所有相关数据的空间。

混淆也会减少Dex文件的大小,除非你是a.a.Az.z.Z类型的开发人员,每个符号需要更少的字符,整体空间就越小。映射混淆的栈跟踪的解决方案,使您可以轻松地诊断您的应用程序中的崩溃。

最后,较小的Dex文件可得到更小的APK,这意味着用户在移动数据上花费较少,并且不太可能放弃下载。如果您提供即时应用程序,那么4Mb的硬性限制意味着保持APK的小尺寸是一个很大的考虑因素。

想了解更多么?

希望这有助于你理解Dex文件,随着D8的出现,它将会变得更小。 如果您有任何问题或意见,请随时与我们联系

猜你喜欢

转载自blog.csdn.net/lihenair/article/details/79037492