瞭解了 Dex 文件之後,對平常開發中遇到一些問題能有更深的理解。如:APK 的瘦身、熱修復、插件化、應用加固、Android 逆向工程、64 K 方法數限制。html
在明白什麼是 Dex 文件以前,要先了解一下 JVM,Dalvik 和 ART。JVM 是 JAVA 虛擬機,用來運行 JAVA 字節碼程序。Dalvik 是 Google 設計的用於 Android平臺的運行時環境,適合移動環境下內存和處理器速度有限的系統。ART 即 Android Runtime,是 Google 爲了替換 Dalvik 設計的新 Android 運行時環境,在Android 4.4推出。ART 比 Dalvik 的性能更好。Android 程序通常使用 Java 語言開發,可是 Dalvik 虛擬機並不支持直接執行 JAVA 字節碼,因此會對編譯生成的 .class 文件進行翻譯、重構、解釋、壓縮等處理,這個處理過程是由 dx 進行處理,處理完成後生成的產物會以 .dex 結尾,稱爲 Dex 文件。Dex 文件格式是專爲 Dalvik 設計的一種壓縮格式。因此能夠簡單的理解爲:Dex 文件是不少 .class 文件處理後的產物,最終能夠在 Android 運行時環境執行。java
java 代碼轉化爲 dex 文件的流程如圖所示,固然真的處理流程不會這麼簡單,這裏只是一個形象的顯示: android
注:圖片來源於網絡算法
如今來經過一個簡單的例子實現 java 代碼到 dex 文件的轉化。api
先來建立一個 Hello.java 文件,爲了便於分析,這裏寫一些簡單的代碼。代碼以下:bash
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 文件雖然已經能夠在 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>:()V
000158: 7010 0400 0100 |0000: invoke-direct {v1}, Ljava/lang/Object;.<init>:()V // method@0004
00015e: 1a00 0b00 |0003: const-string v0, "hello! youzan" // string@000b
000162: 5b10 0000 |0005: iput-object v0, v1, LHello;.helloString:Ljava/lang/String; // field@0000
000166: 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 units
000168: |[000168] Hello.main:([Ljava/lang/String;)V
000178: 2200 0000 |0000: new-instance v0, LHello; // type@0000
00017c: 7010 0000 0000 |0002: invoke-direct {v0}, LHello;.<init>:()V // method@0000
000182: 5401 0000 |0005: iget-object v1, v0, LHello;.helloString:Ljava/lang/String; // field@0000
000186: 6e20 0100 1000 |0007: invoke-virtual {v0, v1}, LHello;.fun:(Ljava/lang/String;)V // method@0001
00018c: 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 units
000190: |[000190] Hello.fun:(Ljava/lang/String;)V
0001a0: 6200 0100 |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0001
0001a4: 6e20 0300 2000 |0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@0003
0001aa: 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 文件的具體格式,就像 MP3,MP4,JPG,PNG 文件同樣,Dex 文件也有它本身的格式,只有遵照了這些格式,才能被 Android 運行時環境正確識別。
Dex 文件總體佈局以下圖所示:
下面將分別對文件頭、索引區、類定義區域進行簡單的介紹。其它區域能夠去 Android 官網瞭解。
文件頭區域決定了該怎樣來讀取這個文件。具體的格式以下表(在文件中排列的順序就是下面表格中的順序):
id 區存儲着字符串,type,prototype,field, method 資源的真正數據在文件中的偏移量,咱們能夠根據 id 區的偏移量去找到該 id 對應的真實數據。
這個區塊是一個偏移量列表,每一個偏移量對應了一個真正的字符串資源,每一個偏移量佔32位。咱們能夠經過偏移量找到對應的實際字符串數據。具體格式以下:
這個區塊是一個索引列表,索引的值對應字符串id區域偏移量列表中的某一項。數據格式以下:
這個區塊是一個方法原型 id 列表,數據格式爲:
這個區塊存儲着原型 id 列表,數據格式爲:
這個區塊存儲着方法 id 列表,數據格式爲: 這個區塊存儲着原型 id 列表,數據格式爲:
這個區域存儲的是類定義的列表,具體的數據結構以下:
這裏推薦一個能夠解析 dex 文件的工具 010 Editor。它能夠經過預置的模板讓咱們更清晰的瞭解 dex 文件的格式。
在目前的主流的 Android 熱修復方案中,Tinker有免費、開源、用戶量大等優勢,所以在有贊也是基於 Tinker 搭建 Android 熱修復服務。Tinker 熱修復的主要原理就是經過對比舊 APK 的 dex 文件與新 APK 的 dex 文件,生成補丁包,而後在 APP 中經過補丁包與舊 APK 的 dex 文件合成新的 dex 文件。流程以下圖所示:
注:圖片來源於 Tinker 官網
Tinker 官方使用自研一套合成方案,就是 DexDiff。它基於 Dex文件格式的特性,具備補丁包小,消耗內存小等優勢。在 DexDiff 算法中,會根據 Dex文件的格式,將 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 文件,而後解決問題。最後,若是你們有任何的建議或意見,歡迎反饋。