APP熱更新方案

大熊先生|網際網路後端技術發表於2017-06-14

為什麼要做熱更新

當一個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年時間做國內超大規模私有云的我來說,感受到了熟悉的味道(伺服器端灰度釋出也是一個套路)

相關文章