Dex格式解析及在Tinker中的應用

Difference

Tinker:全量替換,無須插樁

傳統的熱修復需要插樁實現,插樁的原因和操作:
原因:

1. 通過將補丁dex文件插入到類加載器的dexElement列表最前面,完成熱修復
2. 調用bug類的時候就會先搜索到補丁dex裏的類,從而fix bug
3. bug類和它引用的類都在一個dex中,這個類就被打上了CLASS_ISPREVERIFIED標識,如果這個類調用了插在dexElement隊列前面的補丁dex文件中的同名方法,就會報錯,所以需要阻止bug類打上該標識
4. 通過“插樁”的方法避免需要被fix的class打上CLASS_ISPREVERIFIED標識

如何插樁:
使可能會產生bug的class引用另外一個dex中的class,從而避免該class被打上CLASS_ISPREVERIFIED標識(Groovy語言字節碼織入)。

Tinker使用的是apk全量替換的方法,使用差量包補丁包和原來的apk合成新的apk,使用全新apk,從而不會出現引用其他dex的class的情況,避免了插樁。

Part 1:怎樣生成差量補丁

APK Diff包括:DexDiff,ResDiff,ManifestDiff和SoDiff

1. ManifestDiff

用來檢測新的Manifest是否發送過改變,Tinker不支持Manifest修改。原因應該是apk在執行install操作的時候會向系統註冊Manifest信息,tinker熱修復不會經歷這個過程。

2. ResDiff/SoDiff

BsDiff算法,求新版本和舊版本的二進制差異。原理來自這篇博士論文Naive Differences of Executable Code
基本步驟:
a.對old文件中所有子字符串形成一個字典;
b.對比old文件和new文件,產生diffstring和extra string;
c.將diffstring 和extra string 以及相應的控制字用zip壓縮成一個patch包。

優點:通用性強,適用於所有文件的補丁包生成
缺點:沒有針對特定的格式做優化,導致補丁包可能過大

3. DexDiff

對Dex文件進行差量包生成,傳統的方法有兩種,除了上面提到的Bsdiff算法,還有一種反編譯方法,通過對dex文件反編譯後的class進行比較,以確定哪些class發生了變化的方式,並對發生變化的class文件進行補丁操作。
微信團隊爲了使得差異包最小化,充分利用了Dex的結構,開發了專門應用於Dex文件的差量包生成算法DexDiff算法,跟Bsdiff相比,喪失了通用性,但是效率更高。
DexClassLoader文件結構如下,分爲Header,Table,Data三部分。

Header

dex文件頭部,記錄整個dex文件的相關屬性,如都包含哪些部分(如String,Field,Method,Class等),每部分的大小和偏移量。

Table

存放每種類型數據的地址列表,如在String Table中,連續存放若干個String的地址,根據每個地址,可以在Data段找到改地址存儲的字符串。

Data

存放具體的數據,由Table段不同類型的地址進行索引。

Dex文件結構

Dex Header格式

以String數據爲例,首先讀取Dex Header部分String IDs offset和String IDs Size的內容,如0X70和0X14,代表String數據在Table段的偏移量是0X70,共20個。在Table段讀取這20個數據,每個數據4個字節,根據這4個字節代表的地址,去DataSection找這個地址存儲的內容,解析成String數據。

String數據解析.png

接下來計算新舊Dex的String數據的Diff數據。採用最小序列生成算法,生成由舊String列表生成新列表的操作,用刪除,添加,修改三種操作表示。

算法描述如下,摘抄自這篇帖子

首先我們需要將新舊內容排序,這需要針對排序的數組進行操作

新舊兩個指針,在內容一樣的時候 old、new 指針同時加1,在 old 內容小於 new >內容的時候 old 指針加1,標記當前 old 項爲刪除

在 old 內容大於 new 內容 new 指針加1, 標記當前 new 項爲新增

------old-----
11 foo2
12 foo5
13 hello dodola
14 hello dodola1

------new-----
11 foo3
12 foo5
13 hello dodola1
14 hello dodola3

對比的old cursor 和 new cursor 指針的改變以及操作判定,判定過程如下
old_11 new_11 cmp <0 del
old_12 new_11 cmp >0 add
old_12 new_12 cmp =0 no
old_13 new_13 cmp <0 del
old_14 new_13 cmp =0 no
break;

進入下一步過程
可以確定的是刪除的內容肯定是從 old 中的 index 進行刪除的 添加的內容肯定是從 new 中的 index 中來的,按照這個邏輯我們可以整理如下內容。

old_11 del
new_11 add
old_13 del
new_14 add

到這一步我們需要找出替換的內容,很明顯替換的內容就是從old中del的並且在 >new 中 add 的並且 index 相同的i tem,所以這就簡單了

old_11 replace
old_13 del
new_14 add

這樣就生成了兩個Dex的String部分的變化。

Part 2:怎麼加載新的apk

  1. Dex加載:將合成的新的dex加入到PathClassLoader的dexElements列表中
PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", 
      makePathElements(dexPathList,    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,    suppressedExceptions));
  1. Res加載
    訪問應用程序資源的函數有兩個:getResources和getAssets。getResources返回Resources對象,Resources對象通過資源的ID訪問編譯後的資源。getAssets返回AssetManager對象,AssetManager對象通過資源的文件名訪問編譯後或未經編譯的資源文件。實際上,Resources訪問資源是先通過資源ID獲取文件名,然後通過AssetManager根據文件名訪問資源文件。
    爲了使這兩個方法加載新的資源文件,執行以下操作:
    a. 新建一個AssetManager對象newAssetManager,通過反射調用其addAssetPath方法,傳入新生成的apk文件路徑
    b. 新建Resources對象,通過反射設置其mAssets屬性的值爲newAssetManager
    通過這種方式實現新的資源文件的加載。
addAssetPathMethod.invoke(newAssetManager, externalResourceFile)
assetsFiled.set(resources, newAssetManager);

參考:
https://github.com/WeMobileDev/article/blob/master/微信Android熱補丁實踐演進之路.md
https://www.zybuluo.com/dodola/note/554061
http://www.jianshu.com/p/f7f0a712ddfe
http://blog.csdn.net/add_ada/article/details/51232889

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章