1. 程式人生 > >[譯]Dex檔案格式

[譯]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的出現,它將會變得更小。 如果您有任何問題或意見,請隨時與我們聯絡