Categories
程式開發

大前端時代下的熱修復平台建設


隨著移動需求的增加、移動項目的拓展,如果移動端應用出現Bug不能及時得到修復,影響的不僅僅是用戶體驗,還會造成業務上的損失,因此,建立一套完整的熱修復平台迫在眉睫。基於此,本文作者所在的搜狗商業應用研發團隊構建了一套移動熱修復服務中間件平台,本文從系統架構到主要流程對解決方案進行了詳細的呈現,無論是iOS、Android、RN、Flutter都可以藉助這一思想來開發自己的熱修復平台。

寫在前面

移動應用開發與服務端開發有很大不同,服務端應用如果出現問題,可以通過發布新版本修復,或立即回滾到上一個版本,用戶能夠立刻感知到這一變化;而移動端應用則不同,即使立刻發布新版本修復了問題,也無法保證所有人都能更新到這個版本,如果用戶不升級移動端應用,問題依然得不到修復。

此外,眾所周知,iOS發布App需要經過AppStore的審核,審核的周期幾天甚至一周,雖然近來時間有所縮短,但如果想要快速發布新版本依然避免不了審核;而Android發布App雖然沒有審核的過程,但國內多樣化的應用市場,依然影響著新版本的發布。

因此,針對移動端應用的實時更新是非常必要的需求。隨著移動需求的增加、移動項目的擴展,建立一套完整的熱修復平台日益迫切。

熱修復概念

2015年以來,移動開發領域對熱修復技術的討論和分享越來越多,同時也陸續出現了一些不同的解決方案。業內普遍共識是把不用重新發布新版本,不更新App自身安裝包,在用戶無感知的情況下,就可以對應用當前版本實現bug修復、部分功能修改的技術解決方案稱為熱修復HotFix。

對比常規的開發流程而言,熱修復的開發流程顯得更加靈活方便,優勢很多:

  • 無需重新發​​版,實時高效修復bug;

  • 用戶無感知修復,無需下載新的應用,代價小;

  • 修復成功率高,能把損失降到最低。

實現方式

目前的移動項目中既有使用iOS/Android原生技術進行開發,也有使用React Native/Flutter/Weex等跨平台和H5 Hybrid技術進行開發的。 H5的特性使其不用關注熱更新的問題,原生開發和跨平台開發雖然都能實現熱更新/熱修復,但技術實現手段不盡相同,因此熱修復平台必須能夠提供對上述不同技術方案的支持。熱修復本質上是動態化、插件化技術的一種形式,可以理解為動態加載一個插件,在不同平台由於系統底層實現的不同,採用了不同的實現方式。

iOS系統

用於iOS原生應用熱修復的第三方技術方案主要有JSPatch和waxPatch/LuaView等。主要技術原理是用腳本語言編寫補丁patch,下發給客戶端,在客戶端本地通過Objective-C Runtime在運行時進行類名/方法名反射,替換相應的類和方法實現。

Android系統

普遍的修復原理都基於DEX分包方案,使用了多DEX加載的原理,大致的過程就是:把BUG方法修復以後,放到一個單獨的DEX裡,插入到dexElements數組的最前面,讓虛擬機去加載修復完後的方法。在功能上已經支持類、資源的替換和新增,功能非常強大。

跨平台

目前比較主流的跨平台方案就是React Native、Weex和Flutter了,RN和Weex原理類似都是通過JavaScript語言反射成原生語言代碼去執行,是使用JS腳本語言來編寫的,也就是“即讀即運行”。我們在“讀”之前將之替換成新版本的腳本,運行時執行的便是新的邏輯了。腳本本質上和圖片資源一樣,都是可以進行熱修復的,所以熱修復的原理也比較簡單,只要下發相應的JS補丁包就可以了。業內比較成熟的方案有微軟的CodePush。

Flutter由於出現的比較晚,在Flutter 1.2.1中,Google提供了ResourceUpdater,用來做包的檢查和下載解壓,可以理解為官方支持的熱修復。許多公司為了使用方便也研發了自己的熱修復方案。

存在的問題

由於系統的差異性,不同平台有著不同的解決方案,它們的原理各有不同,適用場景各異,到底採用哪種方案,是開發者比較頭疼的問題。如針對iOS平台的JSPatch、滴滴DynamicCocoa、阿里聚划算LuaView等,Android平台的QQ空間補丁方案、阿里AndFix和Sophix以及微信Tinker等等,當然也可根據技術實現原理自研。

不管使用哪種熱修復技術,我們都需要後台服務的支持,不然就無法實現補丁的分發。由於不同技術團隊選用的技術方案也不一樣,導致存在以下幾個問題:

  • 針對不同客戶端平台需要單獨開發不同的補丁上傳下載後台;

  • 如果更換熱修復技術方案,後台也需要做調整;

  • 客戶端接入邏輯複雜。客戶端接入不同的第三方SDK,需要進行代碼適配;

  • 缺乏數據監控和統計。修復是否成功無法得到相應的數據反饋;

  • 如果管理多個App,需手動管理版本、渠道等多種複雜工作,增加出錯隱患。

解決方案

雖然客戶端由於平台的差異性選擇的熱修復技術不同,但服務端相對而言整體流程和處理策略是統一的,因此,熱修復需要一個後台服務來上傳、管理和分發修復腳本;同時,也要提供針對不同客戶端的SDK,封裝向平台請求腳本、傳輸解密、版本管理等功能。基於以上幾點考慮,我們構建了一套移動熱修復服務中間件平台,主要功能包括:

  • 對iOS、Android原生技術及React Native技術開發的移動應用提供熱修復服務;

  • 提供不同平台的客戶端SDK;

  • 提供分發平台,方便复補丁的上傳和維護;

  • 有補丁的版本控制功能,可以進行更新、回滾操作等;

  • 支持補丁文件全量下發和按條件下發;

  • 補丁分發時進行加密傳輸,保證安全性;

  • 支持數據統計。

系統架構

大前端時代下的熱修復平台建設 1

熱修復服務平台

提供應用管理、應用版本和補丁的管理、修復補丁的上傳和分發、補丁異常時的回滾、傳輸過程中的加密傳輸、自定義加密的RSA密鑰、按條件下發等功能。

客戶端SDK

提供了向後端請求補丁、補丁的版本管理、傳輸後的解密等功能。

主要流程

熱修復涉及的主要模塊是熱修復服務平台和客戶端SDK,核心流程如下圖所示:

大前端時代下的熱修復平台建設 2

通常情況的熱修復流程如下:

  1. 首先需要在熱修復平台創建應用,設置版本號等信息。一般該操作和應用發布、消息推送等功能一起在移動開發平台統一管理;

  2. 添加一個補丁腳本,並選擇所屬應用名、系統和版本,設置下發條件規則等信息;

  3. App啟動同時SDK也啟動,並發送查詢請求給服務端,請求攜帶App標識、App版本號等參數;

  4. 服務端根據收到的請求和條件下發規則判斷下發哪個修復腳本。如果該App在該版本中存在修復腳本,則返回當前生效的腳本信息和地址;

  5. 客戶端SDK根據接口返回數據判斷進行下載、回滾或不進行任何操作;如果未返回任何信息,說明不存在修復腳本需要下發,此時客戶端SDK不進行任何操作;

  6. 客戶端SDK對下載後的補丁腳本進行RSA校驗,並執行腳本;

  7. 補丁腳本執行成功或者失敗等log信息上傳至後台,以供數據分析使用。

應用管理

熱修復是針對App的某個版本進行修復,因此發補丁前必須創建相應的應用和版本。應用創建後會分配一個隨機的appKey,客戶端SDK與服務端交互時必須攜帶appKey,用於標識應用。

大前端時代下的熱修復平台建設 3

補丁管理

補丁列表

補丁列表顯示歷史上發布的補丁信息,可以根據應用名稱和版本號進行查詢。

大前端時代下的熱修復平台建設 4

相關說明如下:

  • 添加新補丁:添加補丁的入口;

  • 應用名稱:顯示應用名稱,來自於創建應用時的輸入;

  • 平台:顯示應用所屬平台,即iOS/Android/React Native;

  • 應用版本號:應用的版本號,來自於添加應用版本時的輸入;

  • 補丁版本號:補丁的版本號,該應用在該版本內的順序遞增;

  • 補丁大小:補丁文件的大小;

  • 補丁描述:來自於添加補丁時的輸入;

  • 補丁狀態:補丁的生效/失效狀態,同一應用同一版本內,只有一個生效狀態的補丁;

  • 下發狀態:補丁的下發狀態,有全量下發和條件下發兩種;

  • 更新事件:該補丁的最近一次操作事件;

  • 顯示內容:顯示補丁的內容,只能顯示iOS補丁的內容;

  • 全量下發:將補丁的下發狀態改為全量下發;

  • 條件下發:將補丁的下發狀態改為條件下發。

添加補丁

在發布一個修復補丁時,要將其上傳至分發平台,並選擇所屬應用名和版本。上傳後,分發平台存儲補丁並存儲補丁的相關信息後,由於一個版本內可能存在多個修復補丁,因此新上傳的補丁會標記為生效,其他的歷史補丁標記為失效狀態。

大前端時代下的熱修復平台建設 5

存儲

服務端由多台機器構成,需要使用統一的文件存儲,不能使用本地的文件系統。

方案一:使用DFS

將補丁文件存儲到DFS中,在MySQL中記錄應用、版本,以及DFS唯一標識的關係,並提供下載接口,用於SDK請求。

方案二:使用CDN

將補丁文件上傳至CDN,客戶端SDK下載時直接訪問CDN的鏈接。 CDN支持高並發,且訪問速度有保障。

考慮到CDN有代碼暴露的風險,傾向於選擇使用DFS。

補丁下發

一個App可能發布了多個版本,一個版本內又可能發布過多個修復補丁。在向SDK下發補丁時,應該下發哪個補丁呢?

下發時遵循如下約定:首先,補丁是基於某個App版本的,App不能跨版本請求補丁;其次,App一個版本內的多個補丁,只下發最新生效的一個。這是由於iOS和Android的熱修復原理決定的。

iOS的熱修復是通過運行時用補丁中的JavaScript代碼動態替換Objective-C代碼實現的,這就造成無法用補丁將App的版本整體升級。因此,在發布下一個版本時必須把上一個版本的補丁中的JS代碼在新版本中用OC再實現一遍。例如,當App處於版本A時某個方法foo出現了問題,在補丁A1中用JS對foo方法進行了修復,在發布下一個App的版本B時,B中foo方法必須用OC重寫一遍。 Android的熱修復是基於基準包,用修復後的新包與基準包diff後生成的補丁,客戶端再加載補丁實現修復,這也要求在發布下一個App版本的時候,必須含有補丁中的內容。

此外,如果在一個版本內下發多個補丁的話,比如版本A中,發現了一個bug,發布了一個修復補丁A1;之後,發現了另一個bug,再發布一個補丁A2。如果A1和A2的內容彼此無關,那麼就要求客戶端SDK要加載多個補丁文件,當補丁之間存在依賴關係時,更需要控制加載的順序,這無疑增加了複雜度,而且無法回滾到特定版本;對於Android而言,由於加載的補丁是基於基準包diff後的包,也做不到加載多個補丁。因此,針對同一版本內有多個補丁的情況,只下發最新的一個生效補丁。

大前端時代下的熱修復平台建設 6

不同App版本不能跨版本下發補丁

大前端時代下的熱修復平台建設 7

同一個App版本內多個補丁時只下發最後生效的版本

回滾

如果所發布的補丁存在問題,這會造成客戶端APP本身出現異常,甚至應用閃退、完全不可用。針對這種情況,有兩種方案,第一是再發布一個新的補丁,補丁中包括修正了的正確代碼。另一種情況是,有可能錯誤難以定位或修正時間太長,根本來不及發布新補丁,那麼必須及時將錯誤補丁回滾。

按照上一節中的下發策略,服務端只會下發當前生效的補丁,因此服務端在回滾的時候只需要簡單地將目標補丁標記為生效即可。

大前端時代下的熱修復平台建設 8

傳輸安全

由於下發的補丁會改變客戶端應用的行為,如果被人攻擊替換代碼,會造成很大危害,因此必須考慮傳輸過程中的安全性。

針對這一問題,設計瞭如下3個解決方案:

方案一:對稱加密

若要讓補丁在傳輸的過程中不會輕易被中間人截獲替換,很容易想到的方式就是對補丁進行加密,可以用 zip 的加密壓縮,也可以用 AES 等加密算法。

優點:實現非常簡單。

缺點:是安全性低,容易被破解。因為密鑰是要保存在客戶端的,只要客戶端被人拿去反編譯,把密碼字段找出來,就完成破解了。

方案二:HTTPS

第二個方案是直接使用 HTTPS 傳輸。

優點:安全性高,只要使用正確,證書在服務端未洩露,就不會被破解。

缺點:部署麻煩,需要服務器支持 HTTPS,門檻較高。另外客戶端需要做好 HTTPS 的證書驗證(有些使用者可能會漏掉這個驗證,導致安全性大降)。如果服務器本來就支持 HTTPS,使用這種方案也是一種不錯的選擇。

方案三:RSA校驗

大前端時代下的熱修復平台建設 9

這種方式屬於數字簽名,用了跟HTTPS 一樣的非對稱加密,只是簡化了,把非對稱加密只用於校驗文件,而不解決傳輸過程中數據內容洩露的問題,而我們的目的只是防止傳輸過程中數據被篡改,對於數據內容洩露並不是太在意。整個校驗過程如下:

  • 服務端計算出補丁文件的 MD5 值,作為這個文件的數字簽名;

  • 服務端通過私鑰加密第 1 步算出的 MD5 值,得到一個加密後的 MD5 值;

  • 把補丁文件和加密後的 MD5 值一起下發給客戶端;

  • 客戶端拿到加密後的 MD5 值,通過保存在客戶端的公鑰解密;

  • 客戶端計算補丁文件的 MD5 值;

  • 對比第 4/5 步的兩個 MD5 值(分別是客戶端和服務端計算出來的 MD5 值),若相等則通過校驗。

只要通過校驗,就能確保補丁在傳輸的過程中沒有被篡改,因為第三方若要篡改補丁文件,必須計算出新的補丁文件MD5 並用私鑰加密,客戶端公鑰才能解密出這個MD5 值,而在服務端未洩露的情況下第三方是拿不到私鑰的。

優點:非對稱加密能夠有效解決傳輸中被篡改的問題。

缺點:數據內容可能會洩露,其實在傳輸過程中不洩露,保存在本地同樣會洩露,若對此在意,可以對補丁文件再加一層簡單的對稱加密。

自定義RSA密鑰

分發平台使用一套默認的公鑰/私鑰進行補丁傳輸過程中的加密/解密,如果對安全性要求更高,可以在上傳補丁時設置自定義的RSA私鑰。

條件下發

很多時候在發布一個補丁時,需要在小範圍內進行驗證,比如特定某個iOS版本或者特定某個用戶;在驗證通過後再進行全網用戶的下發。這時可以用到條件下發。

分發平台在發布補丁時可以選擇使用條件下發,除上傳補丁外,還可以填寫條件語句,只有滿足條件的設備才會執行修復補丁。條件語句由key/value/運算符組成。條件語句的規則與代碼中的條件表達式一致,支持“==、!=、>、=、<=、&&、||”等運算符。如:

iOS>9.0或者userId == 200758 && role == 1

當補丁的下發狀態處於條件下發,且條件語句與SDK上報參數中的條件一致時,才會將補丁發送給該SDK。

計算條件表達式時,如果通過字符串解析和替換的處理等方式,開發繁瑣且實現時不夠優雅。可以使用EL表達式引擎解決這一問題。常見的表達式引擎有Apache Commons中的JEXL(Java Expression Language)、fast-fel等,甚至Java 1.6後自帶的JavaScript腳本引擎也可以完成這個工作。在綜合考量性能和易用性後選擇了JEXL表達式引擎(測試樣例見附錄1)。 JEXL除了支持基本的算術表達式外,也支持在表達式中訪問對象的屬性、訪問數組和集合、調用Java方法等特性,對於表達式的使用有很強的擴展性。

下面是一個JEXL的例子:

大前端時代下的熱修復平台建設 10

數據統計

提供分日、分App、分版本的補丁分發數據統計功能。

SDK設計

iOS/Android和各種跨平台方案只需實現接口的查詢和patch包的下載即可,再根據所採用的熱修復庫實現對應平台的熱修復功能。

查詢接口

大前端時代下的熱修復平台建設 11

下載接口

大前端時代下的熱修復平台建設 12

以iOS為例:

客戶端SDK啟動時會發請求詢問服務端,根據服務端返回數據進行相應處理。客戶端SDK會保存下載到的修復腳本,避免重複下載造成的流量損失。具體流程如下:

大前端時代下的熱修復平台建設 13

  1. App啟動同時SDK也啟動,並發送查詢請求給服務端,請求攜帶App標識、App版本號等參數;

  2. 服務端根據收到的請求判斷下發哪個修復腳本。如果該App在該版本中存在修復腳本,則返回當前生效的腳本信息和地址。客戶端SDK根據接口返回數據判斷進行下載、回滾或不進行任何操作;

  3. 如果未返回任何信息,說明不存在修復腳本需要下發,此時客戶端SDK不進行任何操作;

  4. 如果返回的腳本信息中,腳本的版本號等於本地生效腳本的版本,說明客戶端保存的腳本已是最新的,SDK直接執行本地保存的腳本;

  5. 如果返回的腳本信息中,腳本的版本號不等於本地生效腳本的版本,說明服務端有新的修復腳本發布,或發生了回滾操作,客戶端SDK判斷本地是否存在該版本的腳本,存在時直接執行本地腳本;不存在時發起下載請求獲取腳本,並在本地緩存,然後執行。

爭議

2017年3月,眾多iOS開發者收到蘋果警告郵件,稱其App違規使用動態方法,責令限時整改。這封郵件引起了開發者的恐慌,最後發現問題集中出現在兩個熱更新工具Rollout和JSPatch上。由於JSPatch在iOS業內的高覆蓋率,這個事件影響幾乎波及到了國內所有在AppStore上線的App。

這次警告事件無疑是對iOS平台Native動態化是一次嚴重打擊,其影響甚至可能波及到Android平台,畢竟Google也是禁止加載遠程代碼的,並且執行更為嚴格,只是管不到中國的Android開發而已。

在安卓平台,雖然谷歌沒有能力像蘋果一樣干涉國內的開發,但插件化技術從另一方面遭遇了困境。這一困境就是安卓新版本以及國內各種魔改ROM對於底層的改動。安卓插件化技術依賴部分底層方法以及私有API,而這些在新版本里是很有可能改動的,一旦修改了,插件化就會失效甚至出錯。國內各大手機廠商的系統也喜歡對底層進行修改,它們的修改甚至都不會公開告知,因此兼容問題是插件化技術遇到的最大挑戰。

2018年發布的Android 9.0,甚至要求開發者不得使用私有API,少了這些API,安卓開發被重新關回籠子裡,還能玩的黑科技大大減少,無意之中竟然取得了和蘋果警告類似的效果。

在蘋果「熱修復門」事件之後,iOS動態化的工具都轉入地下發展,關於這方面的研究和分享也急劇減少,甚至連整個iOS技術的分享也變少了。雖然最初通過代碼混淆等可以騙過蘋果審核,但是很快也被禁止了,滴滴聲稱的DynamicCocoa也遲遲沒有開源,QQ甚至開發了一個自己的中間語言OCScript,還開發了一個自己的虛擬機OCSVM去執行它。

雖然使用企業證書發布的App不受應用市場的監管影響,但是整個行業對於熱修復技術的研究和討論也越來越少了,大家不得不尋找新的技術突破點,這種氛圍也間接促進了跨平台技術的推廣。尤其是Flutter發布之後,相較於以前的RN等跨平台技術擁有了更流暢的用戶體驗,許多大廠也開始積極使用。相信在不久的將來移動開發會形成原生開發與跨平台開發並駕齊驅的態勢。

結束語

本文介紹了一種基於第三方或自研的熱修復客戶端技術,但又不強依賴特定服務的通用熱修復中間件管理平台,可以實現安全、穩定、可靠的熱修復補丁上傳、分發、版本管理等功能,並提供完善的數據統計。在實際應用中,可以結合團隊自身技術棧打造成更加通用的熱修復管理平台。