淺析“熱更新”(熱修復)解決方案

天府雲創發表於2018-11-30

新聞事件背景:11月27日,蘋果應用商店集中下架了拼多多、搜狗、科大訊飛、悅跑圈等多家公司的應用產品。科大訊飛和悅跑圈均表示,下架與“熱更新”相關。然而,這並不是蘋果應用商店第一次因為“熱更新”而作出如此大規模的動作。不過,此次多款知名應用遭遇突然下架,也體現出蘋果對其封閉生態系統的強力維護。資料顯示,年初至今蘋果商店中國區單日超萬款APP下架的情況發生過8次以上。在今年5月份和6月份的兩次大清理中,先後有1.4萬款應用和2萬多款應用被下架。

由此可見,此次蘋果下架大量應用,主要針對的應該就是“熱更新”問題。所謂“熱更新”,是指在應用中動態下發程式碼,它可以讓開發者在不釋出新版本的狀態下修復技術缺陷或增添功能,在使用者開啟應用時會自動提醒並下載升級,下載完成後軟體會自動安裝。“熱更新”不需要通過蘋果應用商店軟體版本更新稽核,因此有很多公司選擇利用此方式修改技術缺陷,實現快速迭代。

早在2017年3月份,蘋果就曾對開發者傳送警告郵件,要求停止使用應用“熱更新”功能,否則將會遭到下架處理。該條款於當年6月份生效時,很多知名軟體都曾遭遇過短暫下架。

蘋果為何會對“熱更新”如此嚴防死守?一方面,利用“熱更新”,確實有可能對使用者利益造成侵害。“通過‘熱更新’,可以繞開蘋果的稽核機制,部分開發者有可能會在應用中植入色情、賭博、暴力等違規內容。此外,有些‘熱更新’開發框架存在不安全因素,若有黑客組織發現此類開發框架存在安全漏洞,可以利用後門竊取使用者裝置中的隱私資訊。”APP開發從業者王守強說。

蘋果嚴禁“熱更新”,還有保護其自身商業利益的考慮。成都遊戲開發者馬覓說:“蘋果應用商店的盈利主要來自應用內付費分成,但利用‘熱更新’,開發者有可能繞過蘋果支付體系,這在行業內被稱為‘切支付’,其實就是開發者自己疊加一個支付通道,通過這樣的方式獲得的收入,就可以不與蘋果分成。”

那麼今天我們就來聊聊,專業名詞——熱更新。

熱更新

熱更新是一種各大手遊等眾多APP常用的更新方式。簡單來說,就是在使用者通過App Store下載App之後,開啟App時遇到的即時更新。

2017年6月,AppStore稽核團隊針對AppStore中“熱更新”的App開發者傳送郵件,要求移除所有相關的程式碼、框架或SDK,並重新提交稽核,否則就會在AppStore中下架該軟體。

工作原理

熱更新就是動態下發程式碼,它可以使開發者在不釋出新版本的情況下,修復 BUG 和釋出功能,讓開發者得以繞開蘋果的稽核機制,避免長時間的稽核等待以及多次被拒造成的成本。 

技術特點

    在iOS中有兩種APP更新方式:一種是在APPStore內進行更新,更新時重新下載全部安裝包;另一種就是熱更新,使用者只有在開啟APP時才會發現熱更新包,更新時只需下載安裝更新部分的程式碼,再次開啟時即可。熱更新最大的優點就是快,它可以繞過蘋果方面的稽核,更新通常只需一個晚上即可上線,另一大優點就是更新包較小,一般都在1M左右,使用者不連線WiFi也可隨意下載。

安全隱患

    由於軟體熱更新繞過了蘋果的稽核,黑客開發者有可能會通過提交正常的版本之後,通過熱更新的方式修改APP導致安全隱患,這違反了蘋果的安全隱私政策。另外蘋果此舉既能改善部分使用混編語言的App的流暢性,也能重新掌握一些渠道的App稽核許可權。

APP熱更新方案

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

APP熱更新方案

使用方法:

dexposed提供了3個使用方法:

beforeHookedMethod

afterHookedMethod

replaceHookedMethod

APP熱更新方案

來看看使用方式,也極其簡單.

APP熱更新方案

APP熱更新方案

優缺點:

來說說硬傷吧,不支援art,不支援art,不支援art。

不支援Dalvik 3.0.

所以註定它會逐步失聲,再多的優點也是徒勞

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陣列的最前面,讓虛擬機器去載入修復完後的方法。

APP熱更新方案

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

這個專案之初最大難點在於如何突破Qzone方案的效能問題,通過研究Instant Run的冷插拔與buck的exopackage給了我們靈感。它們的思想都是全量替換新的Dex

APP熱更新方案

因為使用全新的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寫的。

APP熱更新方案

面臨的挑戰:

因為安卓各ROM亂象的原因,ArtMethod的結構可能會不一樣, ArtMethod類包含些什麼其實都是在編譯階段,在執行階段可能不是這麼回事,例如sizeof(ArtMethod)可能實際在各平臺就完全不一樣,但是我們在編譯的時候就確定了值,直接操作容易改亂記憶體資料導致奔潰。

有什麼好的方法來解決這個問題呢?

APP熱更新方案

APP熱更新方案

APP熱更新方案

由於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開發而已。

蘋果是如何檢測的呢,大概可以從給開發者的郵件看出來:

APP熱更新方案

最後我們來看看蘋果的灰度釋出功能吧,對於一個花了將近5年時間做國內超大規模私有云的我來說,感受到了熟悉的味道(伺服器端灰度釋出也是一個套路)

 

  • 熱修復和熱更新1熱更新和熱修復:線上修復程式的BUG2JSPach的使用原理:OC是一門動態執行時的語言,方法的執行和物件的建立是在執行時中建立的.JSPatch正的用執行時,通過JavaScriptCore.framework作為JS引擎,從JS動態呼叫方法和物件到OC中,再作用NSInvocation動態呼叫對應的方法.例   Classclass=NSClassFromString(@"UIViewController"
  • 熱修復和熱更新

    1 熱更新和熱修復:線上修復程式的 BUG

    2 JSPach 的使用原理: OC 是一門動態執行時的語言,方法的執行和物件的建立是在執行時中建立的.JSPatch 正的用執行時,通過JavaScriptCore.framework作為 JS引擎,從 JS 動態呼叫方法和物件到OC 中,再作用NSInvocation動態呼叫對應的方法.例

        Class class = NSClassFromString(@"UIViewController");

        id controller = [class new];

        SEL selector = NSSelectorFromString(@"viewDidLoad");

        [controller performSelector:selector];

    3 使用步驟

                把JSPatch這個資料夾拖入到檔案中然後將在 gitHub 下載的dome.js檔案拖入到專案中,在 APPDelegate中:

    #import "AppDelegate.h"

    #import "JPEngine.h"

    #import "ViewController.h"

     

    @interface AppDelegate ()

     

    @end

     

    @implementation AppDelegate

     

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

        [JPEngine startEngine];

     

        NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"demo.js" ofType:nil];

        [JPEngine evaluateScriptWithPath:jsPath];

     

        self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

        ViewController *rootViewController = [[ViewController alloc] init];

        UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];

        self.window.rootViewController = navigationController;

        [self.window makeKeyAndVisible];

     

        return YES;

    }

     

    @end

     

    並在 ViewController.m 中實現

    - (void)viewDidLoad {

        [super viewDidLoad];

     

        UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, 50)];

        [btn setTitle:@"Push JPTableViewController" forState:UIControlStateNormal];

        [btn addTarget:self action:@selector(handleBtn:) forControlEvents:UIControlEventTouchUpInside];

        [btn setBackgroundColor:[UIColor grayColor]];

        [self.view addSubview:btn];

    }

     

    - (void)handleBtn:(UIButton *)btn {

     

    }

    最後將 dome.js 中的 JSViewController 改為 ViewController 即可

     

    React Native

    掃盲:是一種可以同時操作前段,後臺,移動端都能實時更新開發的技術

    注:通過 JavaSript執行時來建立JavaSript的程式碼

  • 【其他技術方案推薦】

  • 深入淺出 React Native:使用 JavaScript 構建原生應用 - 知乎 https://zhuanlan.zhihu.com/p/19996445

相關文章