HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

浅谈 Android Dex 文件

  • 2020-03-11
  • 本文字数:5559 字

    阅读完需:约 18 分钟

浅谈 Android Dex 文件

概述

为什么要了解 Dex 文件

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

什么是 Dex 文件

在明白什么是 Dex 文件之前,要先了解一下 JVM,Dalvik 和 ART。JVM 是 JAVA 虚拟机,用来运行 JAVA 字节码程序。Dalvik 是 Google 设计的用于 Android 平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在 Android4.4 推出。ART 比 Dalvik 的性能更好。Android 程序一般使用 Java 语言开发,但是 Dalvik 虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式。所以可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。

Dex 文件是怎么生成的

Java 代码转化为 Dex 文件的流程如图所示,当然真的处理流程不会这么简单,这里只是一个形象的显示:



注:图片来源于网络


现在来通过一个简单的例子实现 Java 代码到 Dex 文件的转化。

从.java 到.class

先来创建一个 Hello.java 文件,为了便于分析,这里写一些简单的代码。代码如下:


public class Hello {  private String helloString = "hello! youzan";
public static void main(String[] args) { Hello hello = new Hello(); hello.fun(hello.helloString); }
public void fun(String a) { System.out.println(a); }}
复制代码


在该文件的同级目录下面使用 JDK 的 javac 编译这个 java 文件。


javac Hello
复制代码


javac 命令执行后会在当前目录生成 Hello.class 文件,Hello.class 文件已经可以直接在 JVM 虚拟机上直接执行。这里使用使用命令执行该文件。


java Hello
复制代码


执行后应该会在控制台打印出“hello! youzan”


这里也可以对 Hello.class 文件执行 javap 命令,进行反汇编。


javap -c Hello
复制代码


执行结果如下:


public class Hello { public Hello();  Code:    0: aload_0    1: invokespecial #1         // Method java/lang/Object."<init>":()V    4: aload_0    5: ldc      #2         // String hello! youzan    7: putfield   #3         // Field helloString:Ljava/lang/String;   10: return
public static void main(java.lang.String[]); Code: 0: new #4 // class Hello 3: dup 4: invokespecial #5 // Method "<init>":()V 7: astore_1 8: aload_1 9: aload_1 10: getfield #3 // Field helloString:Ljava/lang/String; 13: invokevirtual #6 // Method fun:(Ljava/lang/String;)V 16: return
public void fun(java.lang.String); Code: 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_1 4: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return}
复制代码


其中 Code 之后都是具体的指令,供 JVM 虚拟机执行。指令的具体含义可以参考 JAVA 官方文档。

从.class 到.dex

上面生成的 .class 文件虽然已经可以在 JVM 环境中运行,但是如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dx 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作。


dx 处理会使用到一个工具 dx.jar,这个文件位于 SDK 中,具体的目录大致为 你的 SDK 根目录/build-tools/任意版本 里面。使用 dx 工具处理上面生成的 Hello.class 文件,在 Hello.class 的目录下使用下面的命令:


dx --dex --output=Hello.dex Hello.class
复制代码


执行完成后,会在当前目录下生成一个 Hello.dex 文件。这个 .dex 文件就可以直接在 Android 运行时环境执行,一般可以通过 PathClassLoader 去加载 dex 文件。现在在当前目录下执行 dexdump 命名来反编译:


dexdump -d Hello.dex
复制代码


执行结果如下(部分区域的含义已经在下面描述):


Processing 'Hello.dex'...Opened 'Hello.dex', DEX version '035'
------ 这里是编写的Hello.java的类的信息 ------Class #0 - Class descriptor : 'LHello;' Access flags : 0x0001 (PUBLIC) Superclass : 'Ljava/lang/Object;' Interfaces - Static fields - Instance fields - #0 : (in LHello;) name : 'helloString' type : 'Ljava/lang/String;' access : 0x0002 (PRIVATE)
------ 下面区域描述的是构造方法的信息。7010 0400 0100 1a00 0b00 之类的数字就是方法中的代码翻译成的指令。Dalvik使用的是16位代码单元,所以这里就是4个数字为一组,每个数字是16进制。invoke-direct 这些是前面指令对应的助记符,也代表着这些指令的真正操作。如果对这些指令转化感兴趣可以去https://source.android.com/devices/tech/dalvik/instruction-formats 查看 ------ Direct methods - #0 : (in LHello;) name : '<init>' --- 方法名称:这个很明显就是构造方法 --- type : '()V' --- 方法原型,()里面表示入参,()后面表示返回值,V代表void--- access : 0x10001 (PUBLIC CONSTRUCTOR) --- 方法访问类型 --- code - registers : 2 --- 方法使用的寄存器数量 --- ins : 1 --- 方法入参,方法除了我们定义的参数以外,系统还会默认带一个特殊参数 --- outs : 1 insns size : 8 16-bit code units --- 指令大小 ---000148: |[000148] Hello.<init>:()V000158: 7010 0400 0100 |0000: invoke-direct {v1}, Ljava/lang/Object;.<init>:()V // method@000400015e: 1a00 0b00 |0003: const-string v0, "hello! youzan" // string@000b000162: 5b10 0000 |0005: iput-object v0, v1, LHello;.helloString:Ljava/lang/String; // field@0000000166: 0e00 |0007: return-void catches : (none) positions : 0x0000 line=1 0x0003 line=2 locals : 0x0000 - 0x0008 reg=1 this LHello;
#1 : (in LHello;) name : 'main' type : '([Ljava/lang/String;)V' access : 0x0009 (PUBLIC STATIC) code - registers : 3 ins : 1 outs : 2 insns size : 11 16-bit code units000168: |[000168] Hello.main:([Ljava/lang/String;)V000178: 2200 0000 |0000: new-instance v0, LHello; // type@000000017c: 7010 0000 0000 |0002: invoke-direct {v0}, LHello;.<init>:()V // method@0000000182: 5401 0000 |0005: iget-object v1, v0, LHello;.helloString:Ljava/lang/String; // field@0000000186: 6e20 0100 1000 |0007: invoke-virtual {v0, v1}, LHello;.fun:(Ljava/lang/String;)V // method@000100018c: 0e00 |000a: return-void catches : (none) positions : 0x0000 line=5 0x0005 line=6 0x000a line=7 locals :
Virtual methods - #0 : (in LHello;) name : 'fun' type : '(Ljava/lang/String;)V' access : 0x0001 (PUBLIC) code - registers : 3 ins : 2 outs : 2 insns size : 6 16-bit code units000190: |[000190] Hello.fun:(Ljava/lang/String;)V0001a0: 6200 0100 |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@00010001a4: 6e20 0300 2000 |0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@00030001aa: 0e00 |0005: return-void catches : (none) positions : 0x0000 line=10 0x0005 line=11 locals : 0x0000 - 0x0006 reg=1 this LHello;
source_file_idx : 1 (Hello.java)
复制代码


到此为止,已经完成了将 Java 代码转变成 Dalvik 可执行的文件,即 dex。

Dex 文件的具体格式

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


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



这些区域的数据互相关联,互相引用。由于篇幅原因,这里只是显示部分区域的关联,完整的请去官网自行查看相关数据整理。下图中的各字段都在后面的各区域的详细介绍中有具体介绍。



下面将分别对文件头、索引区、类定义区域进行简单的介绍。其它区域可以去 Android 官网了解。

文件头

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


id 区

id 区存储着字符串,type,prototype,field, method 资源的真正数据在文件中的偏移量,我们可以根据 id 区的偏移量去找到该 id 对应的真实数据。

字符串 id 区域

这个区块是一个偏移量列表,每个偏移量对应了一个真正的字符串资源,每个偏移量占 32 位。我们可以通过偏移量找到对应的实际字符串数据。具体格式如下:



最终这个偏移的位置应该是落在数据区的。找到这个偏移量的位置后,根据下面的格式就可以读取出这个字符串资源的具体数据:


类型 id 区

这个区块是一个索引列表,索引的值对应字符串 id 区域偏移量列表中的某一项。数据格式如下:



如果我们要找到某个类型的值,需要先根据类型 id 列表中的索引值去字符串 id 列表中找到对应的项,这一项存储的偏移量对应的字符串资源就是这个类型的字符串描述。

方法原型 id 区

这个区块是一个方法原型 id 列表,数据格式为:


成员 id 区

这个区块存储着原型 id 列表,数据格式为:


方法 id 区

这个区块存储着方法 id 列表,数据格式为: 这个区块存储着原型 id 列表,数据格式为:


类定义区

这个区域存储的是类定义的列表,具体的数据结构如下:


解析 dex 文件的工具

这里推荐一个可以解析 dex 文件的工具 010 Editor。它可以通过预置的模板让我们更清晰的了解 dex 文件的格式。


Dex 文件在 Android Tinker 热修复中的应用

在目前的主流的 Android 热修复方案中,Tinker 有免费、开源、用户量大等优点,因此在有赞也是基于 Tinker 搭建 Android 热修复服务。Tinker 热修复的主要原理就是通过对比旧 APK 的 dex 文件与新 APK 的 dex 文件,生成补丁包,然后在 APP 中通过补丁包与旧 APK 的 dex 文件合成新的 dex 文件。流程如下图所示:



注:图片来源于 Tinker 官网

补丁包的生成

Tinker 官方使用自研一套合成方案,就是 DexDiff。它基于 Dex 文件格式的特性,具有补丁包小,消耗内存小等优点。在 DexDiff 算法中,会根据 Dex 文件的格式,将 Dex 文件划分为不同的区块类,如下图:



这些区块有一个统一的数据结构,主要的数据有区块对应的实际数据类型及在文件中的偏移量。如下图:



有了区块数据中的实际数据类型与偏移量,再根据实际数据类型对应的数据结构就可以从文件中读出这个区块包含的实际数据。这里以 header 区域为例,读取代码如下(删除了部分无关代码,代码可以参照上面的 Dex 文件格式的文件头的介绍):


private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException { byte[] magic = headerIn.readByteArray(8);  int apiTarget = DexFormat.magicToApi(magic); checksum = headerIn.readInt();  signature = headerIn.readByteArray(20); fileSize = headerIn.readInt(); int headerSize = headerIn.readInt(); int endianTag = headerIn.readInt(); linkSize = headerIn.readInt(); linkOff = headerIn.readInt(); mapList.off = headerIn.readInt(); stringIds.size = headerIn.readInt(); stringIds.off = headerIn.readInt(); typeIds.size = headerIn.readInt(); typeIds.off = headerIn.readInt(); protoIds.size = headerIn.readInt(); protoIds.off = headerIn.readInt(); fieldIds.size = headerIn.readInt(); fieldIds.off = headerIn.readInt(); methodIds.size = headerIn.readInt(); methodIds.off = headerIn.readInt(); classDefs.size = headerIn.readInt(); classDefs.off = headerIn.readInt(); dataSize = headerIn.readInt(); dataOff = headerIn.readInt();}
复制代码


从文件中读取到新旧 Dex 文件各区块的具体的数据后,就可以进行对比生成补丁包了。因为各区块的数据结构不一致,因此各区块有着相应的 diff 算法来处理各区块补丁生成与合成。算法列表如图:



这些算法会对比新旧 Dex 文件转化成数据结构以后数据的差异,然后生成相关的操作指令,存储到补丁文件,下发到客户端。

补丁的合成

客户端收到补丁文件后,会使用相同的读取方式,将旧 Dex 文件转换为相关的数据结构,然后使用补丁包中的操作指令,对旧 Dex 数据进行修改,生成新 Dex 数据,最后数据写入文件,生成新 Dex 文件,这样就完成了补丁的合成。

写在最后

本文并没有写什么特别深入的东西,对 dex 的文件格式也没有完全描述完全。主要是给大家分享一个 dex 文件的大致结构,还有一些在实际中的应用。让大家在以后遇到相关问题的时候,可以有一些方向去了解 dex 文件,然后解决问题。最后,如果大家有任何的建议或意见,欢迎反馈。

参考资源


Dalvik 和 Java 字节码的对比(http://www.importnew.com/596.html


2020-03-11 22:191343

评论

发布
暂无评论
发现更多内容

Tampermonkey for Mac(油猴Safari浏览器插件) v4.20.6184中文版

Mac相关知识分享

Acrobat Pro DC 2024 for mac (领先的PDF编辑转换器) v24.001.20604

Mac相关知识分享

AutoCAD 2024 for Mac(cad设计绘图) v2024.3中文版

Mac相关知识分享

MES系统在新材料行业中的应用价值

万界星空科技

mes 万界星空科技 万界星空科技mes 新材料mes 新材料行业

Topaz Video AI for mac(地表最强视频无损放大修复工具)v4.1.0版

Mac相关知识分享

.NET 9 预览版 5 发布

EquatorCoco

.net

Flink+Paimon在阿里云大数据云原生运维数仓的实践

Apache Flink

大数据 flink paimon Apache Paimon

VSD Viewer for mac(Visio绘图文件阅读器) v6.16.1版

Mac相关知识分享

软件测试学习笔记丨Allure2报告中添加附件-视频

测试人

软件测试

Termius for Mac(多协议远程管理软件) 8.4.0版

Mac相关知识分享

从IBM ESB升级到RestCloud iPaaS的全面指南

RestCloud

ESB ibm ipaas 数据集成工具

Macs Fan Control Pro for mac(电脑风扇控制软件) v1.5.16中文版

Mac相关知识分享

Lightroom Classic 2024 for Mac(LRC2024) v13.2.0中文版

Mac相关知识分享

手把手系列:小程序插件的开发与引用

FN0

小程序 小程序插件

3分钟快速认识Vue开发小程序的技术原理

Geek_2305a8

热搜资讯API:一键集成,提升你的新闻抓取效率

幂简集成

API 免费 免费API接口 免费API

交易员需要克服的十大心理问题

TechubNews

公式中获灵感,这群研究生在云上攻克铝电解能耗难题

华为云开发者联盟

物联网 华为云 华为云开发者联盟 先锋开发者云上说 企业号2024年7月PK榜

浅谈 Android Dex 文件_文化 & 方法_有赞技术_InfoQ精选文章