為什么要做熱更新
當一個App發布之后,突然發現了一個嚴重bug需要進行緊急修復,這時候公司各方就會忙得焦頭爛額:重新打包App、測試、向各個應用市場和渠道換包、提示用戶升級、用戶下載、覆蓋安裝。
重點是還會有原來的版本遺留,無論你怎么提示都有人放棄治療,不願意升級,強制不能使用體驗又足夠糟糕到讓人不能啟齒。
如果這是一個影響公司收入或者體驗影響極其不好的Bug,那完蛋了,可能公司老板會對整個技術團隊的技術能力喪失信心,其對技術人員的傷害是致命的。
最后最致命的是:
有時候僅僅是因為不小心寫錯了一行代碼,就讓所有的加班都付之東流,苦不苦,冤不冤,想想都苦。
還有一種劇情是研發總監把鍋甩給測試團隊,測試不過關,測試攤攤手說我也不是神啊,總會有漏網之魚。
那能不能神不知鬼不覺在沒有產生較大影響前把bug快速修復了呢?
熱更新的行業情況
先來說說Android
並不是因為Android更有料就先說他,而是它的用戶量級比Iphone大,我們寫文章也是講究大數據分析的不是。
Andoid端在15年熱補丁就比較火,先后出現了
Dexposed、AndFix,Qzone超級補丁,微信的Tinker, 大眾點評的nuwa、百度金融的rocooFix, 餓了么的amigo以及美團的robust。
再來看看Iphone端
技術上要在 iOS 上做到原生動態化比 Android 更容易,iOS 開發語言 Objective-C 天生動態,運行時都能隨意替換方法,運行時加載動態庫又是項很老的技術,只要我把增量的代碼和資源打包到一個 framework 里,動態下發運行時加載,修 bug,加功能都不在話下,性能完全無損,這件事就結束了。
但是呢。蘋果把加載動態庫的功能給封了,動態庫必須跟隨安裝包一起簽名才能被加載,無法通過別的途徑簽名后再下發。
於是有了 waxPatch 和 JSPatch 這樣的方案,以及異軍突起不局限於熱修復Bug而能做主體功能發
布的
React Native 和 Weex
,后面又有了吊口味的滴滴的DynamicCocoa方案和OCScript
Android 熱更新方案
技術派系:
- Native,代表有阿里的Dexposed、AndFix與騰訊的內部方案KKFix;
- Java,代表有Qzone的超級補丁、大眾點評的nuwa、百度金融的rocooFix、餓了么的amigo以及美團的robust。
Native流派與Java流派都有着自己的優缺點,它們具體差異大家可參考下文。事實上從來都沒有最好的方案,只有最適合自己的。
下面我們來一一簡單看下各熱更新的實現方案:
阿里的 DexPosed

阿里開源項目,基於Xposed的AOP框架,方法級粒度,可以進行AOP編程、插樁、熱補丁、SDK hook等功能。
不同的是,Xposed通過劫持 zygote(須root),而dexposed通過劫持 java method (而非樓上說的劫持class loader方法),將java method改變為native,並且將這個方法的實現鏈接到一個通用的Native Dispatch方法上
用處,最大的自然是hotpatch,用這種東西來熱替換某個導致崩潰的方法。手淘還有做的一件事,就是用它作性能監控。這主要得益於無侵入式的方法調用Befor和After事件,能夠讓我們很好的記錄和分析一個方法的調用時間。
開源項目promeG/XLog就是基於dexposed實現的方法調用logging。
優點:運行時、方法級、性能損耗少、學習成本低、本進程
缺點:不支持art、
不支持Dalvik 3.0,
所以注定它會逐步失聲,再多的優點也是徒勞
Qzon的超級補丁 HotFix

該方案基於的是android dex分包方案的,關於dex分包方案本身更多是為了解決Android的64K方法調用限制問題,具體的原因是:
- DexOpt 會把每一個類的方法 id 檢索起來,存在一個鏈表結構里面,但是這個鏈表的長度是用一個 short(2^16=65536) 類型來保存的,導致了方法 id 的數目不能夠超過65536個。當一個項目足夠大的時候,顯然這個方法數的上限是不夠的。
- Dexopt 使用 LinearAlloc 來存儲應用的方法信息。Dalvik LinearAlloc 是一個固定大小的緩沖區。在Android 版本的歷史上,LinearAlloc 分別經歷了4M/5M/8M/16M限制。Android 2.2和2.3的緩沖區只有5MB,Android 4.x提高到了8MB 或16MB。當方法數量過多導致超出緩沖區大小時,也會造成dexopt崩潰
盡管在新版本的 Android 系統中,DexOpt 修復了方法數65K的限制問題,並且擴大了 LinearAlloc 限制,但是這套技術機制保留了下來。
分包的方案簡單來說就是在打包時將應用的代碼分成多個 dex,使得主 dex 的方法數和所需的 LinearAlloc 不超過系統限制。在應用啟動或運行過程中,首先是主 dex 啟動運行后,再加載從 dex,這樣就繞開了這兩個限制。
如何拆分和如何加載可以查看Google官方的方案
MultiDex
Qzon的超級補丁方案玩的是什么招呢?
把BUG方法修復以后,
放到一個單獨的DEX里,插入到dexElements數組的最前面,讓虛擬機去加載修復完后的方法。
Patch.dex中的A.class會優先加載,后續的dex中的A.class就不會加載直接跳過,達到修復目的。
核心問題:
當兩個調用關系的類不在同一個DEX時,就會產生異常報錯。我們知道,在APK安裝時,虛擬機先將classes.dex優化成odex文件后才會執行。在這個過程中,會進行類的verify操作,如果調用關系的類都在同一個DEX中的話就會被打上CLASS_ISPREVERIFIED的標志,然后才會寫入odex文件。具體如何解決這個問題可以參見QQ空間終端開發團隊發布的
安卓App熱補丁動態修復技術介紹。
優點:
- 沒有合成整包(和微信Tinker比起來),產物比較小,比較靈活
- 可以實現類替換,兼容性高
不足:
- 不支持即時生效,必須通過重啟才能生效。
- 為了實現修復這個過程,必須在應用中加入兩個dex!dalvikhack.dex中只有一個類,對性能影響不大,但是對於patch.dex來說,修復的類到了一定數量,就需要花不少的時間加載。對手淘這種航母級應用來說,啟動耗時增加2s以上是不能夠接受的事。
- 在ART模式下,如果類修改了結構,就會出現內存錯亂的問題。為了解決這個問題,就必須把所有相關的調用類、父類子類等等全部加載到patch.dex中,導致補丁包異常的大,進一步增加應用啟動加載的時候,耗時更加嚴重。
微信的 Tinker

這個項目之初最大難點在於如何突破Qzone方案的性能問題,通過研究Instant Run的冷插拔與buck的exopackage給了我們靈感。它們的思想都是全量替換新的Dex
因為使用全新的dex,所以自然繞開了Art地址可能錯亂的問題,在Dalvik模式下也不需要插樁,加載全新的合成dex即可。
焦點問題是合並的過程會不會有問題,會不會耗時或者效率低? 為此騰訊在DEX方面也花了很多時間研究內部的格式以及如何做Merge和進行校驗工作,詳細了解可以查看
大騰訊的第一個開源項目Tinker 這篇文章
優勢:
- 合成整包,不用再構造函數插入代碼,防止verify,verify和opt在編譯期間就已經完成,不會在運行期間進行
- 性能提高。兼容性和穩定性比較高。
- 開發者透明,不需要對包進行額外處理。
不足:
- 與超級補丁技術一樣,不支持即時生效,必須通過重啟應用的方式才能生效。
- 需要給應用開啟新的進程才能進行合並,並且很容易因為內存消耗等原因合並失敗。
- 合並時占用額外磁盤空間,對於多DEX的應用來說,如果修改了多個DEX文件,就需要下發多個patch.dex與對應的classes.dex進行合並操作時這種情況會更嚴重,因此合並過程的失敗率也會更高。
阿里的 Andfix

為何唯獨Andfix能夠做到即時生效呢?
原因是這樣的,在app運行到一半的時候,所有需要發生變更的Class已經被加載過了,在Android上是無法對一個Class進行卸載的。而騰訊系的方案都是讓Classloader去加載新的類。如果不重啟,原來的類還在虛擬機中,就無法加載新類。因此,只有在下次重啟的時候,在還沒走到業務邏輯之前搶先加載補丁中的新類,這樣后續訪問這個類時,就會Resolve為新的類。從而達到熱修復的目的。
Andfix采用的方法是,在已經加載了的類中直接在native層替換掉原有方法,是在原來類的基礎上進行修改的。
以Art為例,每一個Java方法在art中都對應着一個ArtMethod,ArtMethod記錄了這個Java方法的所有信息,包括所屬類、訪問權限、代碼執行地址等等。通過env->FromReflectedMethod,可以由Method對象得到這個方法對應的ArtMethod的真正起始地址。然后就可以把它強轉為ArtMethod指針,從而對其所有成員進行修改。
這很有 C/C++ 研發的味道,實際上Andfix的核心代碼replaceMethod就是用cpp寫的。
IOS的熱更新方案
蘋果把加載動態庫的功能給封了,動態庫必須跟隨安裝包一起簽名才能被加載,無法通過別的途徑簽名后再下發。
Wax
最早要從 Wax 這個項目開始說,大家都知道 Objective-C 有着非常強大的動態特性。比如說:
- 運行時構造類和方法
- 運行時替換方法的實現
實際上這兩個能力是非常恐怖的,像腳本語言那樣,文本即代碼,無須編譯。
后來出現了一個叫做 Wax 的項目(這個項目目前由阿里巴巴維護),這個項目打出的口號是用 Lua 來寫 iOS 原生應用,當然現實中沒有人會這樣干,因為寫起來實在是太痛苦了。但是鑒於 iOS 應用審核比寫 Wax 還痛苦,所以 Wax 成為了做 HotFix 的最佳選擇。
這個項目的做法是通過加載 Lua 腳本,動態的生成 Objective-C 的方法,通常用來替換掉出了問題的那個,Lua 腳本是可以動態下發的,所以也就實現了修復線上 bug 的使命。
當然,Wax 用起來是極為痛苦的,尤其是和 Objective-C 的類型轉換。
JSPatch
iOS 7 的時候 Apple 推出了 JavaScriptCore,這是一個非常有趣的框架,他是 JS 與原生交互的橋梁,讓你在原生和 JS 之間穿梭自如,現在 iOS 平台各種動態技術大多都是基於此。
JSCore 推出不久之后,一個更優秀的項目誕生了:由 bang 寫的 JSPatch。這個項目無疑從各種角度碾壓了 Wax,並且 JS 也比 Lua 更為人熟知,所以也就迅速替代 Wax 成為了熱修復的主流選擇。
JSPatch 的接入成本非常低,對項目的影響也非常小,不需要引入額外的腳本解釋器(因為已經有 JSCore 了),並且 JS 寫起來真的比 Lua 要爽很多。
2017年3月8日,很多iOS開發者收到了警告郵件,聲稱其App違規使用動態方法,責令限時整改,Jspatch一直就被打入冷宮了。
這次警告事件無疑是對iOS平台Native動態化是一次嚴重打擊,其影響甚至可能波及到Android平台,畢竟Google也是禁止加載遠程代碼的,並且執行更為嚴格,只是管不到中國的Android開發而已。
2018-6-9