Dex格式解析及在Tinker中的應用
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段不同類型的地址進行索引。
以String數據爲例,首先讀取Dex Header部分String IDs offset和String IDs Size的內容,如0X70和0X14,代表String數據在Table段的偏移量是0X70,共20個。在Table段讀取這20個數據,每個數據4個字節,根據這4個字節代表的地址,去DataSection找這個地址存儲的內容,解析成String數據。
接下來計算新舊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
- 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));
- 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