tinker源碼研讀(一):補丁生成之DexDiff原理簡析

前言

微信的熱修復(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锉桑,隨后出現(xiàn)的幾起案子排霉,更是在濱河造成了極大的恐慌,老刑警劉巖民轴,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件攻柠,死亡現(xiàn)場離奇詭異,居然都是意外死亡后裸,警方通過查閱死者的電腦和手機瑰钮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來微驶,“玉大人浪谴,你說我怎么就攤上這事∫蚱唬” “怎么了苟耻?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長扶檐。 經(jīng)常有香客問我凶杖,道長,這世上最難降的妖魔是什么款筑? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任智蝠,我火速辦了婚禮,結(jié)果婚禮上奈梳,老公的妹妹穿的比我還像新娘杈湾。我一直安慰自己,他們只是感情好攘须,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布漆撞。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叫挟。 梳的紋絲不亂的頭發(fā)上艰匙,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天限煞,我揣著相機與錄音抹恳,去河邊找鬼。 笑死署驻,一個胖子當(dāng)著我的面吹牛奋献,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播旺上,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瓶蚂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了宣吱?” 一聲冷哼從身側(cè)響起窃这,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎征候,沒想到半個月后杭攻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡疤坝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年兆解,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跑揉。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡锅睛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出历谍,到底是詐尸還是另有隱情现拒,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布望侈,位于F島的核電站具练,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏甜无。R本人自食惡果不足惜扛点,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望岂丘。 院中可真熱鬧陵究,春花似錦、人聲如沸奥帘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至松蒜,卻和暖如春扔茅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秸苗。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工召娜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惊楼。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓玖瘸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親檀咙。 傳聞我的和親對象是個殘疾皇子雅倒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容