1. 程式人生 > >由Sophix引發的Android熱更新底層原理探索

由Sophix引發的Android熱更新底層原理探索

移動網際網路市場日趨成熟,移動產品研發進入平穩發展階段,這意味著開發者的思維和研發模式也應轉入下半程。安全領域技術在開發中的應用一直是作業系統平臺發展週期中的重要一環。熱修復,作為安全領域技術的衍生品,自2016年開始,持續受到關注,並不斷演進。

       2016年上半年,為了提升產品在敏捷開發下的最佳釋出體驗,分別嘗試了備受關注的阿里和微信兩大派系的熱更新方案(支付寶的Andfix和微信的Tinker),但在探索的過程中,發現兩種方案都存在弊端,如使用場景有限,修復成功率低,存在相容問題。加之各方案還在內部快速迭代,均未能達到商業化的標準,所以熱修復在專案中的應用被暫時擱置了。

       直到最近,看到阿里推出了非侵入式熱修復框架Sophix。Sophix對其前輩Andfix,阿里百川Hotfix等方案進行了升級改造,打破了舊方案諸多限制,涵蓋了程式碼修復,資源修復,So庫修復。加上阿里雲平臺的支援,經過簡單的配置就可接入使用,目之所及,Sophix已經成為目前成熟度最高的熱修復框架。這也讓我重新燃起了對熱更新及底層技術探索的熱情。所以我想以此為契機,用系列文章的形式,圍繞熱點技術所涉獵的知識進行由淺入深的持續挖掘。

熱修復的價值

       新技術的層出不窮,適時合理的應用,有助於產品演進並提高生產力。同時,我們應保持冷靜,適合自己的才是最好的,避免新技術解決了老問題,卻帶來更多新問題的尷尬,所以我先從以往應用釋出流程入手,通過分析和比較,逐步瞭解熱修復的技術原理,為專案是否引入做參考。常規釋出流程如下,

這種釋出流程的存在的問題:

1.重新發版,需要重新上架稽核,耗時費力

2.使用者需要重新下載安裝,使用者體驗差

3.Bug修復不及時,成本高

使用熱更新,流程如下

       可見,熱更新能夠以更低的成本,更靈活的方式應對Bug修復。

       非侵入式一直以來都是框架設計的最佳實踐,Sophix同樣以此為核心賣點,它不關心APK build過程,只關注修復後的新包,和原有包,通過視覺化的補丁工具,生成補丁。這一點極大地降低了接入成本,整體流程如下:

       可以看出,Sophix無論對於開發者還是使用者,幾乎沒有侵入。Sophix團隊在文件中提到,他們用巧妙的方式,將低層替換和類載入兩個方案聯合起來,各取所長。我們知道,將某個框架接入專案時,通常會有官方文件會幫助你完成。但隨著框架使用的深入,若總是浮於表面,那麼在出現問題時,則會手足無措。加強對底層技術的探索,對程式設計師來說也是十分有益的。

       前面提到,Sophix是從兩種熱修復方案中演進,那麼我將分別以阿里系低層替換方案和騰訊系的類載入方案為例,從類修復的角度進行分析。

底層替換方案

代表:支付寶的Andfix,話不多說,先上圖,

       如圖,修復bug後,會構建出一個新的apk,通過Andfix框架提供的工具對比出新舊apk classes.dex檔案的差異,並生成patch壓縮包,壓縮包中比較關鍵的是PATCH.MF檔案和diff.dex,虛擬機器通過jarFile讀取PATCH.MF檔案得到補丁類名,dexFile讀取diff.dex,獲得補丁方法。使用補丁類名和補丁方法通過classLoader,找到要修復的bug類名及方法。最後利用hook技術(關於hook會在之後的Dalvik文章中詳述),在native修改指ArtMethod針變數,使其指向補丁方法,從而完成bug修復。

       底層替換的方案的優點是,在類載入後,動態修改native指標,修復即時生效,無需冷啟動。但缺點就是,正因為類已經被載入,記憶體中方法描述符(結構體)已經固定,所以只能替換,不能做新增修復。同時,在Native操作指標時,強轉ArtMethod的型別是AndFix寫死的,無法保證是執行時的ArtMethod結構,這會產生十分嚴重的相容問題。

       通過實踐,發現Andfix修復成功率非常低,時常出現崩潰,補丁無效的現象,經查證,主要還是因為相容問題。而且不能新增field也是主要問題,新增field會破壞序列,這樣一來,呼叫方法時會造成地址混亂。

類載入方案

同樣請先看圖

       不得不承認,Tinker中用了很多巧妙的方法。為了減少補丁大小,Tinker需要通過新舊包生成差量包diff.dex。不同於其他類載入方案,Tinker採用全量更新。客戶端在獲得diff.dex後,會開啟一個service進行dex檔案的合成,合成的全量dex檔案插入到dexElements的第一個位置,dexElements裡面的每個Element實際上就是Dex檔案,classLoader在啟動時,會按順序載入dex檔案,使修復類所在的全量dex包被優先載入,從而完成替換。

       相比較早的增量Dex替換方案(如Qzone超級補丁),Tinker的全量替換很好的規避了觸犯Vm的規則:“當一個類中引用了另外一個類,則一般要求兩個類來自同一個Dex檔案”。增量方案為解決這個問題,需要進行“打樁”,所謂打樁,就是在所有類中分別引用另外Dex檔案中的類,通常的做法是在每一個類中增加構造器並引用另外一個dex中的類(這個dex是為了打樁特意封裝的)。這樣做是防止類被打上CLASS_ISPREVERIFIED標籤,CLASS_ISPREVERIFIED是觸發Vm判定規則的前提。但是,在類載入的最後階段,虛擬機器會對未打上標籤的類‘再次進行校驗和優化,如果在同一時間點載入大量類,那麼就會出現嚴重的效能問題,如啟動時白屏。

       雖然Tinker用全量替換的方式避免了上述問題,但補丁全量合併的過程發生在虛擬機器堆記憶體上,極易造成OOM,導致修復失敗。

       在瞭解兩種熱修復方案之後,我們再次總結一下兩種方案的優缺點,底層替換存在不同定製Rom的相容性問題,同時不能做新增field的修復,但修復可以即可生效。類載入方案在合成全量補丁的時候存在效能問題,修復需要重啟應用(冷啟動),但是相容性較好。Sophix採用底層替換方案為主,類載入方案為次的模式,將二者結合起來。接下來我們來看看Sophix是如何進行雙劍合璧的。

基於底層替換方案的突破

       修復立即生效,是熱修復所追求的,底層替換方案通過在執行時利用hook操作native指標實現“熱”的特性。但這裡有一個關鍵點,底層替換所操作的指標,實際上是ArtMethod,在類被載入,類中的每個方法都會有對應的ArtMethod,它記錄了方法包括所屬類和記憶體地址資訊,Andfix正是通過篡改ArtMethod,將補丁方法ArtMethod的成員值逐一賦給舊方法,實現替換。問題就出現在這個逐一上。因為Andfix的ArtMethod方法結構是根據Android開原始碼寫死的,面對國內廠商的定製,經常會導致兩者ArtMethod方法結構不一致,這也是相容問題產生的根本原因。

       為了解決這個問題,Sophix採用了對舊ArtMethod進行完整替換,通過動態測量ArtMethod的size(通過c層的mempy(dest ,src ,size)方法),進行全量拷貝。這樣做無論ArtMethod被修改成什麼樣,只需要統一執行拷貝,就可以完成替換,完全無視修改虛擬機器導致的ArtMethod結構差異。

另闢蹊徑的冷啟動修復

       底層替換雖能使修復即時生效,但由於類載入後,方法結構已固定,這就造成使用上會有諸多限制。相反類載入方案的使用場景更為廣泛。Sophix使用類載入作為兜底方案。在熱部署無法使用的情況下,自動降級為冷部署方案。

       無論是冷部署還是熱部署,都需要通過同一套補丁兼顧,在Art虛擬機器下,預設支援多dex載入,虛擬機器會優先載入命名為classes.dex的檔案。Sophix利用了這一點,將補丁檔案命名為classes.dex,並對原有dex檔案進行排序。這樣一來,art虛擬機器就會先載入補丁檔案,後續載入的同類名的類會被忽略,最後將載入得到的dexFile把dexElements整體替換。

      Dalvik虛擬機器下情況則有些不同,Dalvik預設只加載classes.dex,其他dex則被忽略。那麼,Sophix就需要一個全量dex,前面提到tinker採用自主研發的dexDiff技術,從方法和指令的維度進行dex合成,但Dex合成過程發生在虛擬機器堆記憶體上,修復的成功率極大的受到效能問題的影響。為了解決這個問題,Sophix換了一種思路,從類的維度,對照補丁包中出現的類,在原有包中做刪除操作,如圖。

       為了避免刪除整個類資訊而導致dex結構發生偏移,所以只對舊包中類的入口進行刪除,實際上類的資訊還在dex包中。這樣一來,冷啟動後,原有的類就不會被載入,相比Tinker的合成方案,Sophix的思路更為輕量化。

       至此,對Sophix對類檔案修復的基本原理描述完畢。可以說Sophix吸取了百家之長,對問題的解決之法堪稱巧妙。但在驚歎阿里技術底蘊的同時,也展現出底層技術的重要性,若沒有對虛擬機器等底層技術的深耕探索,在系統框架的紛繁規則面前,也只能至於庭前止步。

     僅以此篇作為引導,未來將結合原始碼,探索更多底層實現細節。