原理及介紹
現(xiàn)階段,Android熱補(bǔ)丁技術(shù)應(yīng)該是分為以下兩個流派:
- Native:代表有阿里的Dexposed、AndFix與騰訊的內(nèi)部方案KKFix;
- Java:代表有Qzone的超級補(bǔ)丁、大眾點(diǎn)評的nuwa、美團(tuán)的robust、百度金融的rocooFix, 餓了么的amigo。
Native流派與Java流派都有著自己的優(yōu)缺點(diǎn),它們具體差異可參考此文:微信Android熱補(bǔ)丁實(shí)踐演進(jìn)之路 。微信Tinker屬于Java流派。核心思想是利用DexDiff算法對比差異生成Patch補(bǔ)丁包,分平臺合成,全量替換新的Dex。
校驗(yàn)Dex
Tinker它合并完成時完全使用了新的Dex,并且實(shí)現(xiàn)了分平臺合成。這樣既不出現(xiàn)Art地址錯亂的問題,在Dalvik環(huán)境也無須插樁。當(dāng)然考慮到補(bǔ)丁包的體積,我們不能直接將新的Dex放在里面。但可以將新舊兩個Dex的差異放到補(bǔ)丁包中,這里主要調(diào)研的方法有以下幾個:

1.BsDiff;它格式無關(guān),但對Dex效果不是特別好,而且非常不穩(wěn)定。當(dāng)前微信對于so與部分資源,依然使用bsdiff算法;
2.DexMerge;它主要問題在于合成時內(nèi)存占用過大,一個12M的dex,峰值內(nèi)存可能達(dá)到70多M;
3.DexDiff;通過深入Dex格式,實(shí)現(xiàn)一套diff差異小,內(nèi)存占用少以及支持增刪改的算法。
DexDiff算法已經(jīng)非常復(fù)雜,事實(shí)上要實(shí)現(xiàn)分平臺合成并不容易。主要難點(diǎn)有以下幾個方面:
- small dex的類收集;什么類應(yīng)該放在這個小的Dex中呢?
- ClassN處理;對于ClassN怎么樣處理,可能出現(xiàn)類從一個Dex移動到另外一個Dex?
- 偏移二次修正; 補(bǔ)丁包中的操作序列如何二次修正?
- Art.info的大??; 如何修正偏移所引入的info文件的大小?
微信團(tuán)隊(duì)最后實(shí)現(xiàn)了這一套方案,這也是其他全量合成方案所不能做到的:
- Dalvik全量合成,解決了插樁帶來的性能損耗;
- Art平臺合成small dex,解決了全量合成方案占用Rom體積大, OTA升級以及Android N的問題;
- 大部分情況下Art.info僅僅1-20K, 解決由于補(bǔ)丁包可能過大的問題;
事實(shí)上,DexDiff算法變的如此復(fù)雜,怎么樣保證它的正確性呢?微信為此做了以下三件事情:
- 隨機(jī)組成Dex校驗(yàn),覆蓋大部分case;
- 微信200個版本的隨機(jī)Diff校驗(yàn), 覆蓋日常使用情況;
- Dex文件合成產(chǎn)物有效性校驗(yàn),即使算法出現(xiàn)問題,也只是編譯不出補(bǔ)丁包。
每一次DexDiff算法的更新,都需要經(jīng)過以上三個Test才可以提交,這樣DexDiff的這套算法已完成了整個閉環(huán)。
DexDiff 算法
它屬于二路歸并算法,對Dexdiff算法有興趣研究的童鞋可以看看這里:Tinker Dexdiff算法解析
算法過程描述:
1.首先我們需要將新舊內(nèi)容排序,這需要針對排序的數(shù)組進(jìn)行操作。
2.新舊兩個指針,在內(nèi)容一樣的時候 old、new 指針同時加1,在 old 內(nèi)容小于 new 內(nèi)容(注:這里所說的內(nèi)容比較是單純的內(nèi)容比較比如'A'<'a')的時候 old 指針加1 標(biāo)記當(dāng)前 old 項(xiàng)為刪除。
3.在 old 內(nèi)容大于 new 內(nèi)容 new 指針加1, 標(biāo)記當(dāng)前 new 項(xiàng)為新增。
下面是算法執(zhí)行的簡單過程:
------old-----
11 foo2
12 foo5
13 hello dodola
14 hello dodola1
15 hello dodola2
16 hello dodola5
17 out
18 println
------new-----
11 foo3
12 foo5
13 hello dodola1
14 hello dodola3
15 hello dodola_modify
16 out
17 println
對比的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
old_15 new_14 cmp <0 del
old_16 new_14 cmp >0 add
old_16 new_15 cmp <0 del
old_17 new_15 cmp >0 add
old_17 new_16 cmp =0 no
old_18 new_17 cmp =0 no
break;
進(jìn)入下一步過程
可以確定的是刪除的內(nèi)容肯定是從 old 中的 index 進(jìn)行刪除的 添加的內(nèi)容肯定是從 new 中的 index 中來的,按照這個邏輯我們可以整理如下內(nèi)容。
old_11 del
new_11 add
old_13 del
new_14 add
old_15 del
new_15 add
old_16 del
到這一步我們需要找出替換的內(nèi)容,很明顯替換的內(nèi)容就是從 old 中 del 的并且在 new 中 add 的并且 index 相同的i tem,所以這就簡單了
old_11 replace
old_13 del
new_14 add
old_15 replace
old_16 del
ok,到這一步我們就能判定出兩個dex的變化了,很機(jī)智的算法。
運(yùn)行時替換PathClassLoader
事實(shí)上,App image中的class是插入到PathClassloader中的ClassTable中。假設(shè)我們完全廢棄掉PathClassloader,而采用一個新建Classloader來加載后續(xù)的所有類,即可達(dá)到將cache無用化的效果。
需要注意的問題是我們的Application類是一定會通過PathClassloader加載的,所以我們需要將Application類與我們的邏輯解耦,這里方式有兩種:
1.采用類似instant run的實(shí)現(xiàn);在代理application中,反射替換真正的application。這種方式的優(yōu)點(diǎn)在于接入容易,但是這種方式無法保證兼容性,特別在反射失敗的情況,是無法回退的。
2.采用代理Application實(shí)現(xiàn)的方法;即Application的所有實(shí)現(xiàn)都會被代理到其他類,Application類不會再被使用到。這種方式?jīng)]有兼容性的問題,但是會帶來一定的接入成本。
其他的一些問題:
由于原理與系統(tǒng)限制,Tinker有以下已知問題:
- Xposed等微信插件; 市面上有各種各樣的微信插件,它們在微信啟動前會提前加載微信中的類,這會導(dǎo)致兩個問題:
1.Dalvik平臺:出現(xiàn)Class ref in pre-verified class resolved to unexpected implementation的crash;
2.Art平臺:出現(xiàn)部分類使用了舊的代碼,這可能導(dǎo)致補(bǔ)丁無效,或者地址錯亂的問題。
微信在這里的處理方式是:若crash時發(fā)現(xiàn)安裝了Xposed,則立即清除并不再應(yīng)用補(bǔ)丁。
Dex反射成功但是不生效;
部分三星android-19版本存在Dex反射成功,但出現(xiàn)類重復(fù)時,查找順序始終從base.apk開始。 微信在這里的處理方式是增加Dex反射成功校驗(yàn),具體通過在框架中埋入某個類的isPatch變量為false。在補(bǔ)丁時,我們自動將這個變量改為true。通過這個變量最終的數(shù)值,則可以知道反射成功與否。不支持部分三星android-21機(jī)型,加載補(bǔ)丁時會主動拋出"TinkerRuntimeException:checkDexInstall failed";
T不支持修改AndroidManifest.xml,Tinker不支持新增四大組件;
由于Google Play的開發(fā)者條款限制,不建議在GP渠道動態(tài)更新代碼;
在Android N上,補(bǔ)丁對應(yīng)用啟動時間有輕微的影響;
-
對于資源替換,不支持修改remoteView。例如transition動畫,notification icon以及桌面圖標(biāo)。
2017-8-4 更新 :
1.7.11版本或以下版本,項(xiàng)目中若使用了RxJava,在調(diào)用特殊操作符時會報如下錯誤:
java.lang.VerifyError: Rejecting class foc because it failed compile-time verification (declaration of 'foc' appears in /data/user/0/com.xxx.android/tinker/patch-91b25513/dex/classes5.dex.jar)
at euy.zipArray(SourceFile:4567)
at euy.zip(SourceFile:3883)
at euy.zipWith(SourceFile:13590)
at aqi.c(SourceFile:67)
...
具體原因:RxJava里的zip特殊操作符邏輯加上art對aput這個指令的校驗(yàn)方式湊在一起,導(dǎo)致Crash 。Tinker 官方GitHub 項(xiàng)目 Issue 鏈接地址:https://github.com/Tencent/tinker/issues/491
參考文獻(xiàn):
微信移動技術(shù)團(tuán)隊(duì)GitHub博客地址: WeMobileDev/article
Tinker GitHub Wiki :tinker/wiki
Tinker Dexdiff算法解析 : Dexdiff算法解析
Android_N混合編譯與對熱補(bǔ)丁影響:WeMobileDev/article/Android_N