DexDiff:基于dex文件反編譯生成dex增量包

前段時間微信分享了一篇文章——微信Android熱補丁實踐演進之路, 這篇文章主要講了目前流行的Android熱修復方案,同時微信在QZone方案的基礎上優化出一套dex全量替換的熱修復方案(Tinker)。個人認為微信的這套方案盡管規避掉了Qzone方案中插樁導致的問題,但是由于需要在運行時加載全量的dex,這可能會在運行時內存占用上有一定的影響,目前這個僅僅是猜測,有待調研。

GitHub上已有Tinker方案仿版——Tinker_imitator,但帶有個人感情色彩且不負責任地說,這套方案基本是堆砌各項技術然后封裝成套裝,應該屬于比較糙的仿版,方案在生成差分dex的時候采用的是現成的bsdiff算法,這個通用的二進制差分算法很牛逼,但由于不能很好的利用dex本身的結構,因此會導致可能僅僅添加一句代碼也需要下發不小的增量包(參考chrome增量更新小胡瓜中的見解),相對于微信文章中提到的自研DexDiff算法來說,這個就相對弱一點了。

這么一大段背景介紹,我想表達的是我認為微信這篇文章對我而言最關鍵的價值在于DexDiff的概念,看完這篇文章之后,我去學習了bsdiff算法,學習完發現bsdiff算法用于apk的增量更新或許不錯,但是用來做dex的增量包的確是少了許多對dex進行量身定制的優勢,于是我嘗試去思考如何根據dex的數據格式來實現DexDiff,本文主要分享我的一些思路,希望起到拋磚引玉的作用。

dex文件格式

dex文件是運行在Dalvik中的字節碼文件,類似于運行于JVM中的class文件,熟悉class文件格式的同學應該很容易理解,dex文件的布局可以用下圖來進行說明:

dex文件布局

如果想更細致地了解dex文件中每部分數據的具體格式以及意義,請移步至官方文檔Dalvik Executable format,建議可以使用010 Editor學習dex文件,當打開dex文件時該編輯器會自動推薦安裝解析dex文件的插件,安裝完插件便能與dex文件愉快地玩耍了。

反編譯dex文件

了解了dex文件的基本格式之后,就可以開始dex文件的反編譯之旅了,Android反編譯套裝(apktool、smail、dex2jar、jd-gui)現在基本是居家必備了,而今天要出場的大牌就是dex2jar(~~Orz),dex2jar采用了跟asm一樣的套路,搖身一變便成了解析和生成dex文件的神器,建議沒看過asm源碼的Android開發小伙伴,可以直接擼一把dex2jar的源碼,相信會有不少收獲,至少對了解dex文件的內部數據格式來講,會得到量與質的提升。源碼都亮出來了,似乎沒必要在解釋下去了,但是為了能夠圓潤地過渡,我決定還是說明下dex2jar解析完dex文件后的數據結構:

dex2jar解析完dex文件后的數據結構

dex2jar實際上是依據Java中Class的屬性來設計數據結構的,自上而下,一目了然。同時,數據結構中的各項數據都能在dex文件找到對應的數據塊:比如className對應于dex文件格式中的type_id_item,而type_id_item指向的是string_id_item,通過string_id_item中的偏移便可以定位到data section中相應的string_data_item,從而解析出類字符串;再比如annotations對應于dex中的annotation_set_item,而annotation_set_item包含多個annotation_item,每一個annotation_item都可以通過其encoded_annotation解析出對應的type_id_item以及包含的key-value對,從而可以得到修飾類、字段或方法的注解數據。如果想要更全面更細致地了解如何解析dex文件得到上述數據結構集,請擼dex2jar

dex文件的差分與合成

前奏終于結束了,可以開始正題了,不過不要擔心,因為正二八經的內容會比你想象的少很多。DexDiff的目的在于對比新舊dex生成補丁數據,然后利用補丁數據和舊dex合成新dex,目前DexDiff的實現中,我的思路是這樣的:

  1. 基于dex2jar庫反編譯新舊dex
  2. 逐個對比新舊dex中的class:
    • 若class僅存在于舊dex,保存舊dex中的class至deleteClasses
    • 若class僅存在于新dex,保存新dex中的class至replaceClasses
    • 若class存在于新舊dex,但class數據不一致,保存新dex中的class至replaceClasses
  3. 編譯得到的replaceClasses得到replace.dex
  4. 記錄deleteClasses中類的標識字符串得到delete.data
  5. 根據replace.dex、delete.data以及舊dex便可以使用dex2jar編譯得到新dex

其中對比新舊dex中兩個對應的class以確定其是否一致時,采用的方式是逐個對比class中的屬性(accessFlags,superClass,interfaces,fields,methods等),若有一項屬性不一致則定義該class為需要用新dex中數據替換舊dex中對應數據。通過這樣的方式可以實現class粒度上的dex差分,這樣程度的差分可以應用于生成QZone方案中的熱修復補丁包了,如果做增量更新的話,class粒度下的增量包大小也是可以接受的。實現DexDiff的思路就是這么簡單粗暴,但是在實現的過程中還是也還是一波三折,尤其是在比較method中的指令部分時遇到了不少坑,項目源碼已經上傳至GitHub DexDiff

優化思路

目前版本中做差分的粒度是class,但是實際上同樣的思路可以實現method粒度以及instruction粒度的差分。方法級別的差分做法應該可以完全照搬class級別的差分做法;而instruction粒度的差分需要做適當的調整和改進,因為instruction級別不能像class或method一樣可以直接整個替換,我們需要對比指令的語義,并且記錄指令的刪除或插入位置偏移,初步的想法是可以設計自己的數據格式來存儲這些指令差異,但這在我估計總體的實現難度跟class粒度方案比應該不在一個量級,并且應用instruction粒度方案時的整體的穩定性、兼容性等都將是較大的挑戰,單從技術上來講還是值得試的,如果后續完成實踐再分享。

GitHub

DexDiff:求圍觀_

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容