“真正的”Apk增量更新方案ApkDiffPatch

“真正的”Apk增量更新方案ApkDiffPatch

作者: [email protected] 2018.03.31


Android的Apk包增量更新原理:

服務端對新舊版本的2個Apk文件進行diff得到差異部分生成補丁;客戶端只需下載補丁,與已經安裝的舊Apk執行patch就可以生成新版本的Apk進行安裝;達到降低下載流量和節約下載時間的目的。

但現在所有的實現方案几乎都只是把Apk看作文件數據直接用BsDiff(或HDiffPatch)進行diff,而沒有考慮Apk包本身是一個zip壓縮包的事實
比如你只是修改了程序的一行代碼,但很可能造成生成一個過M大小的補丁(典型的Unity遊戲這時補丁甚至可能會上10M);
比如你重新用新工具優化壓縮了新版Apk,但和舊版diff生成補丁後發現和Apk包幾乎一樣大;…
這是因爲在壓縮成Apk(zip)包的時候,壓縮算法已經破壞了“現場”,文件的一處改動或者壓縮參數的變動,將生成完全不同的壓縮編碼;diff算法的優勢沒有真正發揮出來。

ApkDiffPatch方案:

你可以做一個這樣的tar實驗:將新舊Apk當作zip包解壓,並把解壓結果打成2個tar包,然後針對tar用diff算法; 一般情況下你會發現你得到了小得多的補丁!

ApkDiffPatch就是類似該tar實驗的一個增量更新方案的實現,考慮了Apk包本身是一個zip壓縮包的事實:將包抽象成未壓縮數據來進行diff,充分發揮diff算法的優勢。
對比多個Apk的不同版本間求增量的結果來看,新方案的補丁大小一般是直接diff方案的1/3 – 1/10大小! (部分測試結果見

ApkDiffPatch簡介:

ApkDiffPatch是一個通用的Zip包Diff&Patch工具(MIT協議,C++實現,開源地址
支持Zip\Jar\Apk文件的Diff和Patch,生成最小的差異補丁,支持Apk的V2版簽名和Jar(Apk V1)的簽名;
(庫不支持zip64格式,壓縮算法也只支持Deflate,依賴的第三方庫:HDiffPatch, zlib, lzma。)

超簡單的使用方式:

ZipDiff(oldZip,newZip,out diffData)

該函數生成oldZip到newZip的補丁diffData;

ZipPatch(oldZip,diffData,out newZip)

用diffData加上oldZip就可以重新生成newZip;

特別說明

  • 庫默認並不保證patch生成的newZip包和diff時的newZip包二進制完全一致(只保證邏輯上一樣),因爲壓縮算法和zip包的組織方式不一定能夠和原來的完全一致;
  • 但是在使用ApkV2簽名的情況下,二進制完全一致又是必須的要求;
  • 所以庫提供了一個解決方案:用工具ApkNormalized來預處理髮布包:Released newZip := AndroidSDK#apksigner(ApkNormalized(newZip)) before ZipDiff;並且最好不要再改變zlib庫的版本(除非證明兼容);
    當然如果你不需要ApkV2簽名,但也要求patch結果二進制始終一致;也可以這樣用(Released newZip :=ApkNormalized(newZip) before ZipDiff)
  • ZipPatch運行時內存需要4*(decompress stream memory) + ‘ref old decompress memory’ + O(1), 其中oldZip引用到的壓縮文件臨時解壓出的數據’ref old decompress’也可以選擇用臨時磁盤文件來代替內存佔用。

設計和實現:

  • 理論上oldZip包不應被修改,否則patch會失效; 庫在實現的時候,對oldZip的“輕微”修改是兼容的:比如重新調整壓縮率、重新對齊了每個文件起始位置、在CentralDirectory之前或V2簽名區裏插入了渠道數據等;當然對於ApkV2簽名的oldZip包,只允許在V2簽名區中修改;patch的時候會對依賴的“標準”舊數據進行crc32校驗以保證安全;
  • zip包中在CentralDirectory之前或V2簽名區裏是兼容性比較好的能夠寫入自己私有數據的區域,所以庫也不去依賴這一塊數據不能改變;生成的補丁包文件也支持添加附加數據用以在patch時把數據寫到該對應區域(見ZiPatExtraDemo演示代碼);
  • 爲了保證需要的時候控制輸出數據的每一個字節,庫自己實現了一個簡單版本的zip讀寫庫,而不是去依賴minizip等第三方庫;
  • patch的時候需要重新生成zip包,而壓縮一般是比較慢的過程,所以大小和速度需要整體考慮:默認用壓縮率略低但速度較快的壓縮參數;patch時如果可以時就直接拷貝oldZip的壓縮數據;
  • 因爲是FileByFile的diff,所以在patch時也需要對引用到的並且是壓縮狀態的oldZip中的文件先進行解壓,這些數據可以選擇放到內存或寫到臨時文件中;這就要求儘量優化減少這類文件引用量;當前的實現如果發現和newZip中的某文件相同就不會被引用,算是一個“便宜”的可以改進的實現;
  • 庫的底層diff&patch算法選擇了HDiffPatch庫,而不是BsDiff;原因有:
    1. HDiffPatch是我實現的:)
    2. HDiffPatch支持O(1)內存的patch過程(庫當前的默認選擇),而不需要加載O(m+n)的數據到內存,patch速度也快;
    3. diff速度比BsDiff快很多,得到的補丁一般都更小;
    4. HDiffPatch支持選擇壓縮插件,庫默認用了其lzma插件,這樣可以使補丁數據更小;(選擇lzma插件解壓時需要佔用16M內存)
  • 爲了Apk運行時速度,庫支持設置未壓縮文件的數據對齊(比如4或8等),與 AndroidSDK#zipalign 類似;對齊值也會在diff運行時進行識別(運行時也會識別可能的壓縮參數);
  • patch過程在寫zip時,設計上不使用Data Descriptor信息的方式保存文件壓縮後大小,而是用了一些方式儘量(保存某些數據的壓縮後大小)提早寫入正確的壓縮大小,這樣有利於patch速度和Apk包性能;
  • newZip包進行V2重簽名後,V1簽名文件會被簽名工具重新生成,也就是會重新壓縮這幾個文件;這時diff時會嘗試能否兼容其壓縮(並計算出兼容的壓縮參數),如果能兼容那就可以和其他數據一起執行解壓狀態的diff(補丁更小),否則就保持其壓縮狀態的diff;
  • 現在的解決方案無法解決這樣的應用場景:Apk必須使用V2簽名,而自己又無法簽名;比如自己是下載服務商,包不是自己進行的V2簽名;比如將自己的Apk交給了渠道商V2簽名發佈了;這種場景實際上自己已經沒有了控制力;(要在這種情況下實現類似ApkDiffPatch的方案的可能實現途徑討論

    歡迎提交bug和建議

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