APP熱更新方案
爲什麼要做熱更新
當一個App發佈之後,突然發現了一個嚴重bug需要進行緊急修復,這時候公司各方就會忙得焦頭爛額:重新打包App、測試、向各個應用市場和渠道換包、提示用戶升級、用戶下載、覆蓋安裝。
重點是還會有原來的版本遺留,無論你怎麼提示都有人放棄治療,不願意升級,強制不能使用體驗又足夠糟糕到讓人不能啓齒。
如果這是一個影響公司收入或者體驗影響極其不好的Bug,那完蛋了,可能公司老闆會對整個技術團隊的技術能力喪失信心,其對技術人員的傷害是致命的。
最後最致命的是:
有時候僅僅是因爲不小心寫錯了一行代碼,就讓所有的加班都付之東流,苦不苦,冤不冤,想想都苦。
還有一種劇情是研發總監把鍋甩給測試團隊,測試不過關,測試攤攤手說我也不是神啊,總會有漏網之魚.
那能不能神不知鬼不覺再沒有產生較大影響前把bug快速修復了呢?
熱更新的行業情況
先來說說Android
並不是因爲Android更有料就先說他,而是它的用戶量級比Iphone大,我們寫文章也是講究大數據分析的不是..
Andoid端在15年熱補丁就比較火,先後出現了Dexposed、AndFix,Qzone超級補丁的類Nuwa方式,微信的Tinker, 大衆點評的nuwa、百度金融的rocooFix, 餓了麼的amigo以及美團的robust.
再來看看Iphone端
技術上要在 iOS 上做到原生動態化比 Android 更容易,iOS 開發語言 Objective-C 天生動態,運行時都能隨意替換方法,運行時加載動態庫又是項很老的技術,只要我把增量的代碼和資源打包到一個 framework 裏,動態下發運行時加載,修 bug,加功能都不在話下,性能完全無損,這件事就結束了。
但是呢。蘋果把加載動態庫的功能給封了,動態庫必須跟隨安裝包一起簽名才能被加載,無法通過別的途徑簽名後再下發。
於是有了 waxPatch 和 JSPatch 這樣的方案,以及異軍突起不侷限於熱修復Bug而能做主體功能發佈的React Native 和 Weex,後面又有了吊口味的滴滴的DynamicCocoa方案和OCScript
熱更新的技術原理
先來說JAVA
技術派系:
• 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
使用方法:
dexposed提供了3個使用方法:
beforeHookedMethod
afterHookedMethod
replaceHookedMethod
來看看使用方式,也極其簡單.
優缺點:
來說說硬傷吧,不支持art,不支持art,不支持art。
不支持Dalvik 3.0.
所以註定它會逐步失聲,再多的優點也是徒勞
插播一條硬廣: 技術文章轉發收錄太多,此文出處 http://www.cnblogs.com/Creator/ 以及微信公衆號: 互聯網手藝人
Qzon的超級補丁方案
該方案基於的是android dex分包方案的,關於dex分包方案本身更多是爲了解決Android的64K方法調用限制問題,具體的原因是:
• DexOpt 會把每一個類的方法 id 檢索起來,存在一個鏈表結構裏面,但是這個鏈表的長度是用一個 short 類型來保存的,導致了方法 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
http://developer.android.com/intl/zh-cn/tools/building/multidex.htm
Qzon的超級補丁方案玩的是什麼招呢?
把BUG方法修復以後,放到一個單獨的DEX裏,插入到dexElements數組的最前面,讓虛擬機去加載修復完後的方法。
Patch.dex中的A.class會有優先加載,後續的dex中的A.class就不會加載直接跳過,達到修復目的。
核心問題:
當兩個調用關係的類不在同一個DEX時,就會產生異常報錯。我們知道,在APK安裝時,虛擬機需要將classes.dex優化成odex文件,然後纔會執行。在這個過程中,會進行類的verify操作,如果調用關係的類都在同一個DEX中的話就會被打上CLASS_ISPREVERIFIED的標誌,然後纔會寫入odex文件。具體如何解決這個問題可以參見QQ空間終端開發團隊QQ空間終端開發團隊發佈的” 安卓App熱補丁動態修復技術介紹”
優缺點:
1.沒有合成整包(和微信Tinker比起來),產物比較小,比較靈活
2.可以實現類替換,兼容性高。(某些三星手機不起作用)
不足:
1.不支持即時生效,必須通過重啓才能生效。
2.爲了實現修復這個過程,必須在應用中加入兩個dex!dalvikhack.dex中只有一個類,對性能影響不大,但是對於patch.dex來說,修復的類到了一定數量,就需要花不少的時間加載。對手淘這種航母級應用來說,啓動耗時增加2s以上是不能夠接受的事。
3.在ART模式下,如果類修改了結構,就會出現內存錯亂的問題。爲了解決這個問題,就必須把所有相關的調用類、父類子類等等全部加載到patch.dex中,導致補丁包異常的大,進一步增加應用啓動加載的時候,耗時更加嚴重。
微信Tinker
根據微信內部人士介紹:微信tinker項目之初最大難點在於如何突破Qzone方案的性能問題,通過研究Instant Run的冷插拔與buck的exopackage給了我們靈感。它們的思想都是全量替換新的Dex
因爲使用全新的dex,所以自然繞開了Art地址可能錯亂的問題,在Dalvik模式下也不需要插樁,加載全新的合成dex即可。
焦點問題是合併的過程會不會有問題,會不會耗時或者效率低? 爲此騰訊在DEX方面也花了很多時間研究內部的格式以及如何做Merge和進行校驗工作,詳細瞭解可以查看” 大騰訊的第一個開源項目「Tinker」”這篇文章
優勢:
1. 合成整包,不用在構造函數插入代碼,防止verify,verify和opt在編譯期間就已經完成,不會在運行期間進行
2. 性能提高。兼容性和穩定性比較高。
3. 開發者透明,不需要對包進行額外處理。
不足:
1. 與超級補丁技術一樣,不支持即時生效,必須通過重啓應用的方式才能生效。
2. 需要給應用開啓新的進程才能進行合併,並且很容易因爲內存消耗等原因合併失敗。
3. 合併時佔用額外磁盤空間,對於多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寫的。
面臨的挑戰:
因爲安卓各ROM亂象的原因,ArtMethod的結構可能會不一樣, ArtMethod類包含些什麼其實都是在編譯階段,在運行階段可能不是這麼回事,例如sizeof(ArtMethod)可能實際在各平臺就完全不一樣,但是我們在編譯的時候就確定了值,直接操作容易改亂內存數據導致奔潰。
有什麼好的方法來解決這個問題呢?
來看看奇技淫巧
由於f1和f2都是static方法,所以都屬於direct ArtMethod Array。由於NativeStructsModel類中只存在這兩個方法,因此它們肯定是相鄰的。
那麼我們就可以在JNI層取得它們地址的差值:
然後,就以這個methSize作爲sizeof(ArtMethod),代入之前的代碼。
問題就迎刃而解了。即使以後的Android版本不斷修改ArtMethod的成員,只要保證ArtMethod數組仍是以線性結構排列就能完美兼容。
著:此方法最新方案並不在開源的方案中
最大的優勢在於
1. BUG修復的即時性
2. 補丁包同樣採用差量技術,生成的PATCH體積小
3. 對應用無侵入,幾乎無性能損耗
不足:
1. 不支持新增字段,以及修改<init>方法,也不支持對資源的替換。
再來看看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 要爽很多。
3月8日,很多iOS開發者發了警告郵件,聲稱其App違規使用動態方法,責令限時整改,Jspatch一直就被打入冷宮了
這次警告事件無疑是對iOS平臺Native動態化是一次嚴重打擊,其影響甚至可能波及到Android平臺,畢竟Google也是禁止加載遠程代碼的,並且執行更爲嚴格,只是管不到中國的Android開發而已。
滴滴的DynamicCocoa
DynamicCocoa這種方案,繞了一個更大的道,從編譯階段入手,通過 clang 把 OC 代碼編譯成自己定製的 JS 格式,再動態下發去執行,做到原生開發,動態運行,主打動態添加功能,當然順便把修 bug 也給支持了。手機 QQ 內部也有一個類似的方案,不過更進一步,他們通過 clang 把 OC 代碼編譯成自己定製的字節碼動態下發,然後開發一個虛擬機去執行(驚呆了),同樣實現了原生開發,動態運行,都是 NB 得很的方案。只要底層處理做得足夠好,也是個成本低收益高的方案,不過目前都還沒開源,在github上是一個只有兩行README但是有1000+Star的神奇項目
DynamicCocoa與Jspatch 思路上都是實現 JS 和 OC 的互調:DynamicCocoa 的重點是動態化能力,優勢在於完全不用寫 JS 和更多的語法特性支持;對於 HotPatch 來說 JSPatch 是更加小巧、輕量的解決方案。
據說在滴滴 App 已經上線並使用了好幾個版本,如滴滴小巴、專車接送機都有過 10k 級別的動態化模塊上線。
20170612 蘋果已經正式禁止熱更新,給涉及到檢測出來的開發者發了郵件,同時提供 App Store “自動更新的分階段發佈” 功能。
蘋果是如何檢測的呢,大概可以從給開發者的郵件看出來:
最後我們來看看蘋果的灰度發佈功能吧,對於一個花了將近3年時間做國內超大規模私有云的我來說,感受到了熟悉的味道(服務器端灰度發佈也是一個套路)