為什么要做熱更新
當(dāng)一個(gè)App發(fā)布之后,突然發(fā)現(xiàn)了一個(gè)嚴(yán)重bug需要進(jìn)行緊急修復(fù),這時(shí)候公司各方就會(huì)忙得焦頭爛額:重新打包App、測試、向各個(gè)應(yīng)用市場和渠道換包、提示用戶升級、用戶下載、覆蓋安裝。
重點(diǎn)是還會(huì)有原來的版本遺留,無論你怎么提示都有人放棄治療,不愿意升級,強(qiáng)制不能使用體驗(yàn)又足夠糟糕到讓人不能啟齒。
如果這是一個(gè)影響公司收入或者體驗(yàn)影響極其不好的Bug,那完蛋了,可能公司老板會(huì)對整個(gè)技術(shù)團(tuán)隊(duì)的技術(shù)能力喪失信心,其對技術(shù)人員的傷害是致命的。
最后最致命的是:
有時(shí)候僅僅是因?yàn)椴恍⌒膶戝e(cuò)了一行代碼,就讓所有的加班都付之東流,苦不苦,冤不冤,想想都苦。
還有一種劇情是研發(fā)總監(jiān)把鍋甩給測試團(tuán)隊(duì),測試不過關(guān),測試攤攤手說我也不是神啊,總會(huì)有漏網(wǎng)之魚.
那能不能神不知鬼不覺再?zèng)]有產(chǎn)生較大影響前把bug快速修復(fù)了呢?
熱更新的行業(yè)情況
先來說說Android
并不是因?yàn)锳ndroid更有料就先說他,而是它的用戶量級比Iphone大,我們寫文章也是講究大數(shù)據(jù)分析的不是..
Andoid端在15年熱補(bǔ)丁就比較火,先后出現(xiàn)了Dexposed、AndFix,Qzone超級補(bǔ)丁的類Nuwa方式,微信的Tinker, 大眾點(diǎn)評的nuwa、百度金融的rocooFix, 餓了么的amigo以及美團(tuán)的robust.
再來看看Iphone端
技術(shù)上要在 iOS 上做到原生動(dòng)態(tài)化比 Android 更容易,iOS 開發(fā)語言 Objective-C 天生動(dòng)態(tài),運(yùn)行時(shí)都能隨意替換方法,運(yùn)行時(shí)加載動(dòng)態(tài)庫又是項(xiàng)很老的技術(shù),只要我把增量的代碼和資源打包到一個(gè) framework 里,動(dòng)態(tài)下發(fā)運(yùn)行時(shí)加載,修 bug,加功能都不在話下,性能完全無損,這件事就結(jié)束了。
但是呢。蘋果把加載動(dòng)態(tài)庫的功能給封了,動(dòng)態(tài)庫必須跟隨安裝包一起簽名才能被加載,無法通過別的途徑簽名后再下發(fā)。
于是有了 waxPatch 和 JSPatch 這樣的方案,以及異軍突起不局限于熱修復(fù)Bug而能做主體功能發(fā)布的React Native 和 Weex,后面又有了吊口味的滴滴的DynamicCocoa方案和OCScript
熱更新的技術(shù)原理
先來說JAVA
技術(shù)派系:
• Native,代表有阿里的Dexposed、AndFix與騰訊的內(nèi)部方案KKFix;
• Java,代表有Qzone的超級補(bǔ)丁、大眾點(diǎn)評的nuwa、百度金融的rocooFix, 餓了么的amigo以及美團(tuán)的robust。
Native流派與Java流派都有著自己的優(yōu)缺點(diǎn),它們具體差異大家可參考上文。事實(shí)上從來都沒有最好的方案,只有最適合自己的。
下面我們來一一簡單看下各熱更新的實(shí)現(xiàn)方案:
Dexposed
阿里開源項(xiàng)目,基于Xposed的AOP框架,方法級粒度,可以進(jìn)行AOP編程、插樁、熱補(bǔ)丁、SDK hook等功能。
不同的是,Xposed通過劫持 zygote(須root),而dexposed通過劫持 java method ( 而非樓上說的劫持class loader方法),將java method改變?yōu)閚ative,并且將這個(gè)方法的實(shí)現(xiàn)鏈接到一個(gè)通用的Native Dispatch方法上.)用處,最大的自然是hotpatch,用這種東西來熱替換某個(gè)導(dǎo)致崩潰的方法。手淘還有做的一件事,就是用它作性能監(jiān)控。這主要得益于無侵入式的方法調(diào)用Befor和After事件,能夠讓我們很好的記錄和分析一個(gè)方法的調(diào)用時(shí)間。開源項(xiàng)目promeG/XLog就是基于dexposed實(shí)現(xiàn)的方法調(diào)用logging
使用方法:
dexposed提供了3個(gè)使用方法:
beforeHookedMethod
afterHookedMethod
replaceHookedMethod
來看看使用方式,也極其簡單.
優(yōu)缺點(diǎn):
來說說硬傷吧,不支持art,不支持art,不支持art。
不支持Dalvik 3.0.
所以注定它會(huì)逐步失聲,再多的優(yōu)點(diǎn)也是徒勞
插播一條硬廣: 技術(shù)文章轉(zhuǎn)發(fā)收錄太多,此文出處 http://www.cnblogs.com/Creator/ 以及微信公眾號: 互聯(lián)網(wǎng)手藝人
Qzon的超級補(bǔ)丁方案
該方案基于的是android dex分包方案的,關(guān)于dex分包方案本身更多是為了解決Android的64K方法調(diào)用限制問題,具體的原因是:
• DexOpt 會(huì)把每一個(gè)類的方法 id 檢索起來,存在一個(gè)鏈表結(jié)構(gòu)里面,但是這個(gè)鏈表的長度是用一個(gè) short 類型來保存的,導(dǎo)致了方法 id 的數(shù)目不能夠超過65536個(gè)。當(dāng)一個(gè)項(xiàng)目足夠大的時(shí)候,顯然這個(gè)方法數(shù)的上限是不夠的。
•Dexopt 使用 LinearAlloc 來存儲(chǔ)應(yīng)用的方法信息。Dalvik LinearAlloc 是一個(gè)固定大小的緩沖區(qū)。在Android 版本的歷史上,LinearAlloc 分別經(jīng)歷了4M/5M/8M/16M限制。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB 或16MB。當(dāng)方法數(shù)量過多導(dǎo)致超出緩沖區(qū)大小時(shí),也會(huì)造成dexopt崩潰
盡管在新版本的 Android 系統(tǒng)中,DexOpt 修復(fù)了方法數(shù)65K的限制問題,并且擴(kuò)大了 LinearAlloc 限制,但是這套技術(shù)機(jī)制保留了下來
分包的方案簡單來說就是在打包時(shí)將應(yīng)用的代碼分成多個(gè) dex,使得主 dex 的方法數(shù)和所需的 LinearAlloc 不超過系統(tǒng)限制。在應(yīng)用啟動(dòng)或運(yùn)行過程中,首先是主 dex 啟動(dòng)運(yùn)行后,再加載從 dex,這樣就繞開了這兩個(gè)限制。
如何拆分和如何加載可以查看Google官方的方案MultiDex
http://developer.android.com/intl/zh-cn/tools/building/multidex.htm
Qzon的超級補(bǔ)丁方案玩的是什么招呢?
把BUG方法修復(fù)以后,放到一個(gè)單獨(dú)的DEX里,插入到dexElements數(shù)組的最前面,讓虛擬機(jī)去加載修復(fù)完后的方法。
Patch.dex中的A.class會(huì)有優(yōu)先加載,后續(xù)的dex中的A.class就不會(huì)加載直接跳過,達(dá)到修復(fù)目的。
核心問題:
當(dāng)兩個(gè)調(diào)用關(guān)系的類不在同一個(gè)DEX時(shí),就會(huì)產(chǎn)生異常報(bào)錯(cuò)。我們知道,在APK安裝時(shí),虛擬機(jī)需要將classes.dex優(yōu)化成odex文件,然后才會(huì)執(zhí)行。在這個(gè)過程中,會(huì)進(jìn)行類的verify操作,如果調(diào)用關(guān)系的類都在同一個(gè)DEX中的話就會(huì)被打上CLASS_ISPREVERIFIED的標(biāo)志,然后才會(huì)寫入odex文件。具體如何解決這個(gè)問題可以參見QQ空間終端開發(fā)團(tuán)隊(duì)QQ空間終端開發(fā)團(tuán)隊(duì)發(fā)布的” 安卓App熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)介紹”
優(yōu)缺點(diǎn):
1.沒有合成整包(和微信Tinker比起來),產(chǎn)物比較小,比較靈活
2.可以實(shí)現(xiàn)類替換,兼容性高。(某些三星手機(jī)不起作用)
不足:
1.不支持即時(shí)生效,必須通過重啟才能生效。
2.為了實(shí)現(xiàn)修復(fù)這個(gè)過程,必須在應(yīng)用中加入兩個(gè)dex!dalvikhack.dex中只有一個(gè)類,對性能影響不大,但是對于patch.dex來說,修復(fù)的類到了一定數(shù)量,就需要花不少的時(shí)間加載。對手淘這種航母級應(yīng)用來說,啟動(dòng)耗時(shí)增加2s以上是不能夠接受的事。
3.在ART模式下,如果類修改了結(jié)構(gòu),就會(huì)出現(xiàn)內(nèi)存錯(cuò)亂的問題。為了解決這個(gè)問題,就必須把所有相關(guān)的調(diào)用類、父類子類等等全部加載到patch.dex中,導(dǎo)致補(bǔ)丁包異常的大,進(jìn)一步增加應(yīng)用啟動(dòng)加載的時(shí)候,耗時(shí)更加嚴(yán)重。
微信Tinker
根據(jù)微信內(nèi)部人士介紹:微信tinker項(xiàng)目之初最大難點(diǎn)在于如何突破Qzone方案的性能問題,通過研究Instant Run的冷插拔與buck的exopackage給了我們靈感。它們的思想都是全量替換新的Dex
因?yàn)槭褂萌碌膁ex,所以自然繞開了Art地址可能錯(cuò)亂的問題,在Dalvik模式下也不需要插樁,加載全新的合成dex即可。
焦點(diǎn)問題是合并的過程會(huì)不會(huì)有問題,會(huì)不會(huì)耗時(shí)或者效率低? 為此騰訊在DEX方面也花了很多時(shí)間研究內(nèi)部的格式以及如何做Merge和進(jìn)行校驗(yàn)工作,詳細(xì)了解可以查看” 大騰訊的第一個(gè)開源項(xiàng)目「Tinker」”這篇文章
優(yōu)勢:
1. 合成整包,不用在構(gòu)造函數(shù)插入代碼,防止verify,verify和opt在編譯期間就已經(jīng)完成,不會(huì)在運(yùn)行期間進(jìn)行
2. 性能提高。兼容性和穩(wěn)定性比較高。
3. 開發(fā)者透明,不需要對包進(jìn)行額外處理。
不足:
1. 與超級補(bǔ)丁技術(shù)一樣,不支持即時(shí)生效,必須通過重啟應(yīng)用的方式才能生效。
2. 需要給應(yīng)用開啟新的進(jìn)程才能進(jìn)行合并,并且很容易因?yàn)閮?nèi)存消耗等原因合并失敗。
3. 合并時(shí)占用額外磁盤空間,對于多DEX的應(yīng)用來說,如果修改了多個(gè)DEX文件,就需要下發(fā)多個(gè)patch.dex與對應(yīng)的classes.dex進(jìn)行合并操作時(shí)這種情況會(huì)更嚴(yán)重,因此合并過程的失敗率也會(huì)更高。
阿里Andfix方案
為何唯獨(dú)Andfix能夠做到即時(shí)生效呢?
原因是這樣的,在app運(yùn)行到一半的時(shí)候,所有需要發(fā)生變更的Class已經(jīng)被加載過了,在Android上是無法對一個(gè)Class進(jìn)行卸載的。而騰訊系的方案,都是讓Classloader去加載新的類。如果不重啟,原來的類還在虛擬機(jī)中,就無法加載新類。因此,只有在下次重啟的時(shí)候,在還沒走到業(yè)務(wù)邏輯之前搶先加載補(bǔ)丁中的新類,這樣后續(xù)訪問這個(gè)類時(shí),就會(huì)Resolve為新的類。從而達(dá)到熱修復(fù)的目的。
Andfix采用的方法是,在已經(jīng)加載了的類中直接在native層替換掉原有方法,是在原來類的基礎(chǔ)上進(jìn)行修改的。
以Art為例,每一個(gè)Java方法在art中都對應(yīng)著一個(gè)ArtMethod,ArtMethod記錄了這個(gè)Java方法的所有信息,包括所屬類、訪問權(quán)限、代碼執(zhí)行地址等等。通過env->FromReflectedMethod,可以由Method對象得到這個(gè)方法對應(yīng)的ArtMethod的真正起始地址。然后就可以把它強(qiáng)轉(zhuǎn)為ArtMethod指針,從而對其所有成員進(jìn)行修改。
這很C/C++ 研發(fā)的味道,實(shí)際上Andfix的核心代碼replaceMethod就是用cpp寫的。
面臨的挑戰(zhàn):
因?yàn)榘沧扛鱎OM亂象的原因,ArtMethod的結(jié)構(gòu)可能會(huì)不一樣, ArtMethod類包含些什么其實(shí)都是在編譯階段,在運(yùn)行階段可能不是這么回事,例如sizeof(ArtMethod)可能實(shí)際在各平臺(tái)就完全不一樣,但是我們在編譯的時(shí)候就確定了值,直接操作容易改亂內(nèi)存數(shù)據(jù)導(dǎo)致奔潰。
有什么好的方法來解決這個(gè)問題呢?
來看看奇技淫巧
由于f1和f2都是static方法,所以都屬于direct ArtMethod Array。由于NativeStructsModel類中只存在這兩個(gè)方法,因此它們肯定是相鄰的。
那么我們就可以在JNI層取得它們地址的差值:
然后,就以這個(gè)methSize作為sizeof(ArtMethod),代入之前的代碼。
問題就迎刃而解了。即使以后的Android版本不斷修改ArtMethod的成員,只要保證ArtMethod數(shù)組仍是以線性結(jié)構(gòu)排列就能完美兼容。
著:此方法最新方案并不在開源的方案中
最大的優(yōu)勢在于
1. BUG修復(fù)的即時(shí)性
2. 補(bǔ)丁包同樣采用差量技術(shù),生成的PATCH體積小
3. 對應(yīng)用無侵入,幾乎無性能損耗
不足:
1. 不支持新增字段,以及修改<init>方法,也不支持對資源的替換。
再來看看IOS的熱更新技術(shù):
蘋果把加載動(dòng)態(tài)庫的功能給封了,動(dòng)態(tài)庫必須跟隨安裝包一起簽名才能被加載,無法通過別的途徑簽名后再下發(fā)。
Wax
最早要從 Wax 這個(gè)項(xiàng)目開始說,大家都知道 Objective-C 有著非常強(qiáng)大的動(dòng)態(tài)特性。比如說:
•運(yùn)行時(shí)構(gòu)造類和方法
•運(yùn)行時(shí)替換方法的實(shí)現(xiàn)實(shí)際上這兩個(gè)能力是非??植赖南衲_本語言那樣,文本即代碼,無須編譯。后來出現(xiàn)了一個(gè)叫做 Wax的項(xiàng)目(這個(gè)項(xiàng)目目前由阿里巴巴維護(hù)),這個(gè)項(xiàng)目打出的口號是用 Lua 來寫 iOS 原生應(yīng)用,當(dāng)然現(xiàn)實(shí)中沒有人會(huì)這樣干,因?yàn)閷懫饋韺?shí)在是太痛苦了。但是鑒于 iOS 應(yīng)用審核比寫 Wax 還痛苦,所以 Wax 成為了做 HotFix 的最佳選擇。
這個(gè)項(xiàng)目的做法是通過加載 Lua 腳本,動(dòng)態(tài)的生成 Objective-C 的方法,通常用來替換掉出了問題的那個(gè),Lua 腳本是可以動(dòng)態(tài)下發(fā)的,所以也就實(shí)現(xiàn)了修復(fù)線上 bug 的使命。
當(dāng)然,Wax 用起來是極為痛苦的,尤其是和 Objective-C 的類型轉(zhuǎn)換。
JSPatch
iOS 7 的時(shí)候 Apple 推出了 JavaScriptCore,這是一個(gè)非常有趣的框架,他是 JS 與原生交互的橋梁,讓你在原生和 JS 之間穿梭自如,現(xiàn)在 iOS 平臺(tái)各種動(dòng)態(tài)技術(shù)大多都是基于此。
JSCore 推出不久之后,一個(gè)更優(yōu)秀的項(xiàng)目誕生了:由 bang 寫的 JSPatch。這個(gè)項(xiàng)目無疑從各種角度碾壓了 Wax,并且 JS 也比 Lua 更為人熟知,所以也就迅速替代 Wax 成為了熱修復(fù)的主流選擇。
JSPatch 的接入成本非常低,對項(xiàng)目的影響也非常小,不需要引入額外的腳本解釋器(因?yàn)橐呀?jīng)有 JSCore 了),并且 JS 寫起來真的比 Lua 要爽很多。
3月8日,很多iOS開發(fā)者發(fā)了警告郵件,聲稱其App違規(guī)使用動(dòng)態(tài)方法,責(zé)令限時(shí)整改,Jspatch一直就被打入冷宮了
這次警告事件無疑是對iOS平臺(tái)Native動(dòng)態(tài)化是一次嚴(yán)重打擊,其影響甚至可能波及到Android平臺(tái),畢竟Google也是禁止加載遠(yuǎn)程代碼的,并且執(zhí)行更為嚴(yán)格,只是管不到中國的Android開發(fā)而已。
滴滴的DynamicCocoa
DynamicCocoa這種方案,繞了一個(gè)更大的道,從編譯階段入手,通過 clang 把 OC 代碼編譯成自己定制的 JS 格式,再動(dòng)態(tài)下發(fā)去執(zhí)行,做到原生開發(fā),動(dòng)態(tài)運(yùn)行,主打動(dòng)態(tài)添加功能,當(dāng)然順便把修 bug 也給支持了。手機(jī) QQ 內(nèi)部也有一個(gè)類似的方案,不過更進(jìn)一步,他們通過 clang 把 OC 代碼編譯成自己定制的字節(jié)碼動(dòng)態(tài)下發(fā),然后開發(fā)一個(gè)虛擬機(jī)去執(zhí)行(驚呆了),同樣實(shí)現(xiàn)了原生開發(fā),動(dòng)態(tài)運(yùn)行,都是 NB 得很的方案。只要底層處理做得足夠好,也是個(gè)成本低收益高的方案,不過目前都還沒開源,在github上是一個(gè)只有兩行README但是有1000+Star的神奇項(xiàng)目
DynamicCocoa與Jspatch 思路上都是實(shí)現(xiàn) JS 和 OC 的互調(diào):DynamicCocoa 的重點(diǎn)是動(dòng)態(tài)化能力,優(yōu)勢在于完全不用寫 JS 和更多的語法特性支持;對于 HotPatch 來說 JSPatch 是更加小巧、輕量的解決方案。
據(jù)說在滴滴 App 已經(jīng)上線并使用了好幾個(gè)版本,如滴滴小巴、專車接送機(jī)都有過 10k 級別的動(dòng)態(tài)化模塊上線。
20170612 蘋果已經(jīng)正式禁止熱更新,給涉及到檢測出來的開發(fā)者發(fā)了郵件,同時(shí)提供 App Store “自動(dòng)更新的分階段發(fā)布” 功能。
蘋果是如何檢測的呢,大概可以從給開發(fā)者的郵件看出來:
最后我們來看看蘋果的灰度發(fā)布功能吧,對于一個(gè)花了將近3年時(shí)間做國內(nèi)超大規(guī)模私有云的我來說,感受到了熟悉的味道(服務(wù)器端灰度發(fā)布也是一個(gè)套路)