前言
微信的熱修復(fù)框架Tinker已經(jīng)在國慶節(jié)之前開源了,成為了github.com/Tecent下第一個項目,刷爆了各位開發(fā)者的朋友圈畅蹂。作為一個超級APP的HotFix庫垒拢,Tinker不僅值得我們compile旬迹,更值得我們read。
原理
Tinker和以往的HotFix庫思路不太一樣求类,它更像是APP的增量更新奔垦,在服務(wù)器端通過差異性算法,計算出新舊dex之間的差異包尸疆,推送到客戶端宴倍,進行合成。傳統(tǒng)的差異性算法有BsDiff仓技,而Tinker的牛逼之處就在于它自己基于Dex的文件格式鸵贬,研發(fā)出了DexDiff算法。
補丁生成之DexDiff
補丁生成是在編譯階段進行的脖捻,當(dāng)我們在build.gradle中配置好tinkerOldApkPath后阔逼,調(diào)用tinkerPatchDebug,補丁就開始生成了地沮。
調(diào)用鏈為:
TinkerPatchSchemaTask.tinkerPatch()
->Runner.gradleRun()
->Runner.run()
->Runner.tinkerPatch()
->ApkDecoder.patch()
......
->DexPatchGenerator.executeAndSaveTo()
executeAndSaveTo()方法是DexDiff算法的真正入口嗜浮,DexDiff算法的特點在于它深入分析了Dex文件格式,深度利用Dex的格式來減少差異大小摩疑。
在了解DexDiff之前危融,我們需要對Dex文件有個初步了解,Dex的文件格式如下表雷袋。參考自這里:Dalvik可執(zhí)行格式(dex)上
數(shù)據(jù)名稱 | 解釋 |
---|---|
header | dex文件頭部吉殃,記錄整個dex文件的相關(guān)屬性 |
string_ids | 字符串標(biāo)識列表,涉及了文件中所用到的所有字符串楷怒,包括內(nèi)部名稱(比如類型描述符)或者代碼引用的常量對象蛋勺。這個列表根據(jù)字符串內(nèi)容按照UTF-16(避免了各地區(qū)語言差異導(dǎo)致的不同)來進行排序,不得包含重復(fù)條目 |
type_ids | 類型標(biāo)識列表鸠删,涉及了文件中所有用到的類型(如類抱完、數(shù)組或者原始類型),不論是否是在文件中定義刃泡。這個列表必須按照類型字符串在string_ids中的索引來排序巧娱,不得含有重復(fù)條目碉怔。 |
proto_ids | 方法原型標(biāo)識列表涉及了文件中所有用到的方法原型。列表按返回值類型在type_ids中的索引進行排序禁添,索引相同的話再按參數(shù)類型在type_ids中的索引排序眨层,不得含有重復(fù)條目。 |
field_ids | 類屬性(類成員)標(biāo)識列表上荡,涉及了文件中所有用到的類屬性趴樱,不論其是否在文件中定義。列表依次按照所在類的類型(按type_ids索引)酪捡、屬性名(按string_ids索引)叁征、自身類型(按type_ids索引)進行排序,不得含有重復(fù)條目逛薇。 |
method_ids | 方法標(biāo)識列表捺疼,涉及了文件中所有用到的方法,不論是否在文件中定義永罚。列表依次按照方法所在類的類型(按type_ids索引)啤呼、方法名(按string_ids索引)、方法原型(按proto_ids索引)呢袱,不得含有重復(fù)條目官扣。 |
class_defs | 類定義列表,列表的順序必須符合一個類的基類以及其所實現(xiàn)的接口在這個類的前面這一規(guī)則羞福。此外惕蹄,列表中出現(xiàn)多個同名類的定義是無效的。 |
data | 數(shù)據(jù)區(qū)治专,包含上述各個結(jié)構(gòu)所需的所有支持?jǐn)?shù)據(jù)卖陵。不同的條目有不同的數(shù)據(jù)對齊要求,如果有需要张峰,會在條目之前插入若干字節(jié)以滿足合適的對齊 |
link_data | 用于靜態(tài)鏈接文件的數(shù)據(jù)泪蔫,數(shù)據(jù)的類型其實是不確定的,對于不存在鏈接的文件喘批,這個區(qū)域是空的撩荣,此外不同的運行時實現(xiàn)也會對這一區(qū)域的數(shù)據(jù)格式做相應(yīng)修改。 |
想要深入了解Dex文件格式的可以看Google官方文檔:https://source.android.com/devices/tech/dalvik/dex-format.html谤祖,《Android軟件安全與逆向分析》這本書里關(guān)于Dex文件也有很詳細(xì)的講解婿滓。
了解了Dex的格式后老速,讓我們來看一下DexDiff的基本步驟粥喜。
DexDiff的主要步驟如下:
Step1:計算出new dex中每項Section的大小,比如string_ids在dex文件中所占大小橘券。
int patchedStringIdsSize = newDex.getTableOfContents().stringIds.size * SizeOf.STRING_ID_ITEM;
step2:根據(jù)表中前一項的偏移地址和大小额湘,計算出每項Section的偏移地址卿吐。
this.patchedStringIdsOffset = patchedHeaderOffset + patchedheaderSize;
step3:調(diào)用DexSectionDiffAlgorithm.execute(),將new dex與old dex中的每項section進行對比锋华,對于每項Section嗡官,遍歷其每一項Item,進行新舊對比毯焕,記錄ADD衍腥,DEL標(biāo)識,存放于patchOperationList中纳猫。接著遍歷patchOperationList婆咸,添加REPLACE標(biāo)識,最后將ADD芜辕,DEL尚骄,REPLACE操作分別記錄到各自的List中。
這里面的新舊對比的算法很有趣侵续,它是直接將oldItem.compareTo(newItem)倔丈,結(jié)果小于0記為DEL,大于0記為ADD状蜗。為什么可以直接這樣比較呢需五?別忘啦,從上面Dex文件格式表中可知轧坎,Dex文件中的Section都是經(jīng)過排序的警儒。
上圖中,old dex中下標(biāo)為2的item "c" 被DEL眶根,c后面的元素前移蜀铲,new dex中下標(biāo)5處ADD一個item "f",而在下標(biāo)8處又ADD一個item "j"属百。
下標(biāo)5的"f"在這里被記為ADD记劝,但是它其實是REPLACE了"g"。在之后遍歷patchOperationList時族扰,算法會判斷operation前一個opearation(prevPatchOperation)的類型厌丑,若prevPatchOperation為DEL,而自己剛好為ADD渔呵,那么就將自己的類型改為REPLACE怒竿。
Iterator<PatchOperation<T>> patchOperationIt = this.patchOperationList.iterator();
PatchOperation<T> prevPatchOperation = null;
while (patchOperationIt.hasNext()) {
PatchOperation<T> patchOperation = patchOperationIt.next();
if (prevPatchOperation != null
&& prevPatchOperation.op == PatchOperation.OP_DEL
&& patchOperation.op == PatchOperation.OP_ADD
) {
if (prevPatchOperation.index == patchOperation.index) {
prevPatchOperation.op = PatchOperation.OP_REPLACE;
prevPatchOperation.newItem = patchOperation.newItem;
patchOperationIt.remove();
prevPatchOperation = null;
} else {
prevPatchOperation = patchOperation;
}
} else {
prevPatchOperation = patchOperation;
}
}
step4:調(diào)用DexPatchGenerator.writePatchOperations(),將記錄寫入補丁扩氢。
對于DEL:
開辟一塊DelOpIndexList大小的區(qū)域(DelOpIndexList中記錄了每塊要刪除部分的索引),遍歷記錄DelOpIndexList耕驰,對于每一個DEL索引,計算出其相對于前一個DEL索引的偏移录豺,記錄偏移量朦肘。
buffer.writeUleb128(delOpIndexList.size());
int lastIndex = 0;
for (Integer index : delOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
對于ADD和REPLACE饭弓,也會進行和DEL一樣的操作。
buffer.writeUleb128(addOpIndexList.size());
lastIndex = 0;
for (Integer index : addOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
buffer.writeUleb128(replaceOpIndexList.size());
lastIndex = 0;
for (Integer index : replaceOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
不同的一點是媒抠,ADD和REPALACE還會接著寫入新增和替換的item弟断。
for (T newItem : newItemList) {
if (newItem instanceof StringData) {
buffer.writeStringData((StringData) newItem);
} else
if (newItem instanceof Integer) {
// TypeId item.
buffer.writeInt((Integer) newItem);
} else
if (newItem instanceof TypeList) {
buffer.writeTypeList((TypeList) newItem);
}
......
}
總結(jié)
這里只是簡單的介紹了DexDiff的簡要過程,中間其實省去了很多細(xì)節(jié)趴生,而這些細(xì)節(jié)與Dex文件格式聯(lián)系非常緊密阀趴,想要徹底的了解DexDiff原理,還需好好研究Dex文件苍匆。
(轉(zhuǎn)載請注明ID:半棧工程師舍咖,歡迎訪問個人博客:https://halfstackdeveloper.github.io/)
歡迎關(guān)注我的知乎專欄:https://zhuanlan.zhihu.com/halfstack