探索Android開源框架 - 11. 熱修復原理

語言: CN / TW / HK

熱修復技術介紹

  • 重新發布版本代價大,成本高,不及時,使用者體驗差,對此有幾種解決方案:

  1. Hybird:原生+H5混合開發,缺點是人工成本搞,使用者體驗不如純原生方案好;

  2. 外掛化:移植成本高,對老程式碼的改造費時費力,而且無法動態修改;

  3. 熱修復技術,將補丁上傳到雲端,app可以直接從雲端下來補丁直接應用;

  • 熱修復技術對於國內開發者來說是一個比較實用的功能,可以解決如下問題:

  1. 釋出新版本代價較大,使用者下載安裝成本高;

  2. 版本更新的效率問題,需要較長時間來完成版本覆蓋;

  3. 版本更新的升級率問題,不升級版本的使用者得不到修復,強更又比較暴力。

  4. 小而重要的功能,需要短時間內完成版本覆蓋,比如節日活動。

  • 熱修復的優勢:無需發版,使用者無感知,修復成功率高,用時短;

百家爭鳴的熱修復框架

  • 手淘的Dexposed: 開源,底層替換方案, 基於Xposed,針對Dalvik執行時的Java Method Hook技術,但對於Dalvik底層過於依賴,無法繼續相容Android5.0之後的ART,因此作罷;

  • 支付寶的Andfix:開源,底層替換方案,藉助Dexposed思想,做到了Dalvik和ART環境的全版本相容,但其底層固定結構的替換方案穩定性不好,使用範圍也存在著諸多限制,而且對於資源和so修復未能實現,詳細原理參考:Android熱修復框架AndFix原理解析及使用;

  • 阿里百川的Hotfix:開源,底層替換方案,依賴於Andfix並對業務邏輯解耦,安全性和易用性較好,但還是存在Andfix的缺點;

  • Qzone超級補丁: 未開源,類載入方案,會侵入打包流程

  • 美團的Robust:開源,Instant Run方案,詳細可以參考美團技術團隊的文章及Robust原始碼:Android熱更新方案Robust, Android熱更新方案Robust開源,新增自動化補丁工具

  • 大眾點評的Nuwa: 開源,類載入方案,具體實現可以參考:Android 熱修復Nuwa的原理及Gradle外掛原始碼解析

  • 餓了麼的Amigo:開源,類載入方案

  • 微信的Tinker:開源,類載入方案,關於Tinker的原理可以看一下鴻洋的文章:Android 熱修復 Tinker接入及原始碼淺析, Android 熱修復 Tinker 原始碼分析之DexDiff / DexPatch, Android 熱修復 Tinker Gradle Plugin解析

  • 手淘的Sophix:未開源,商業收費,類載入方案+底層替換方案;(手淘團隊基於Sophix有整理出一本電子書:深入探索Android熱修復技術原理, 其中不僅講了熱修復原理還有許多編譯相關的內容(關注微信公眾號今陽說,回覆關鍵字"熱修復"領取書籍pdf))

熱修復技術原理

  • 熱修復框架的核心技術主要有三類,分別是程式碼修復、資源修復和動態連結庫修復

程式碼修復:

  • 程式碼修復主要有三個方案,分別是底層替換方案、類載入方案和Instant Run方案

1. 類載入方案

  • 類載入方案需要重啟App後讓ClassLoader重新載入新的類,因為類是無法被解除安裝的,要想重新載入新的類就需要重啟App,因此採用類載入方案的熱修復框架是不能即時生效的。

優點:

  • 不需要太多的適配;

  • 實現簡單,沒有諸多限制;

缺點

  • 需要APP重啟才能生效(冷啟動修復);

  • dex插樁:Dalvik平臺存在插樁導致的效能損耗,Art平臺由於地址偏移問題導致補丁包可能過大的問題;

  • dex替換:Dex合併記憶體消耗在vm head上,可能OOM,導致合併失敗

  • 虛擬機器在安裝期間為類打上CLASS_ISPREVERIFIED標誌是為了提高效能的,強制防止類被打上標誌會影響效能;

Dex分包

  • 類載入方案基於Dex分包方案,而Dex分包方案主要是為了解決65536限制和LinearAlloc限制:

  1. 65536限制:DVM指令集的方法呼叫指令invoke-kind索引為16bits,最多能引用 65535個方法;

  2. LinearAlloc限制:DVM中的LinearAlloc是一個固定的快取區,當方法數過多超出了快取區的大小,安裝時提示INSTALL_FAILED_DEXOPT;

  • Dex分包方案: 打包時將應用程式碼分成多個Dex,將應用啟動時必須用到的類和這些類的直接引用類放到主Dex中,其他程式碼放到次Dex中。當應用啟動時先載入主Dex,等到應用啟動後再動態的載入次Dex。主要有兩種方案,分別是Google官方方案、Dex自動拆包和動態載入方案。

ClassLoader

  • 在本系列的上一篇文章 探索Android開源框架 - 10. 外掛化原理 中有講過java中的ClassLoader(載入jar檔案和Class檔案,本質是載入Class檔案), android中的ClassLoader(載入dex檔案和apk檔案), 雙親委派機制,以及ClassLoader如何載入外掛中的類,其實熱修復中程式碼修復的類載入方案也是使用的同樣的原理;

幾種不同的實現:

  1. 將補丁包放在Element陣列的第一個元素得到優先載入(QQ空間的超級補丁和Nuwa)

  2. 將補丁包中每個dex 對應的Element取出來,之後組成新的Element陣列,在執行時通過反射用新的Element陣列替換掉現有的Element 陣列(餓了麼的Amigo);

  3. 將新舊apk做了diff,得到patch.dex,然後將patch.dex與手機中apk的classes.dex做合併,生成新的classes.dex,然後在執行時通過反射將classes.dex放在Element陣列的第一個元素(微信Tinker)

  4. Sophix:dex的比較粒度在類的維度,並且 重新編排了包中dex的順序,classes.dex,classes2.dex..,可以看作是 dex檔案級別的類插樁方案,對舊包中的dex順序進行打破重組

2. 底層替換方案

  • 其思想來源於Xposed框架,完美詮釋了AOP程式設計,直接在Native層修改原有類(不需要重啟APP),由於是在原有類進行修改限制會比較多,不能夠增減原有類的方法和欄位,因為這破壞原有類的結構(引起索引變化), 雖然限制多,但時效性好,載入輕快,立即見效;

優點

  • 實時生效,不需要重新啟動,載入輕快

缺點

  • 相容性差,由於 Android 系統每個版本的實現都有差別,所以需要做很多的相容。

  • 開發需要掌握 jni 相關知識, 而且native異常排查難度更高

  • 由於無法新增方法和欄位,無法做到功能釋出級別

幾種不同的實現:

  1. 採用替換ArtMethod結構體中的欄位,這樣會有相容問題,因為手機廠商的修改 以及 android版本的迭代可能會導致底層ArtMethod結構的差異,導致方法替換失敗;(AndFix)

  2. 同時使用類載入和底層替換方案,針對小修改,在底層替換方案限制範 圍內,還會再判斷所執行的機型是否支援底層替換方案,是就採用底層替換(替換整個ArtMethod結構體,這樣不會存在相容問題),否則使用類載入替換;(Sophix)

3. Instant Run方案

Instant Run新特性的原理就是當進行程式碼改動之後,會進行增量構建,也就是僅僅構建這部分改變的程式碼,並將這部分程式碼以補丁的形式增量地部署到裝置上,然後進行程式碼的熱替換,從而觀察到程式碼替換所帶來的效果。其實從某種意義上講,Instant Run和熱修復在本質上是一樣的。

Instant Run打包邏輯

  • 接入Instant Run之後,與傳統方式相比,在進行打包的時候會存在以下四個不同點

  1. manifest注入:InstantRun會生成一個自己的application,然後將這個application註冊到manifest配置檔案裡面,這樣就可以在其中做一系列準備工作,然後再執行業務程式碼;

  2. nstant Run程式碼放入主dex:manifest注入之後,會將Instant Run的程式碼放入到Android虛擬機器第一個載入的dex檔案中,包括classes.dex和classes2.dex,這兩個dex檔案存放的都是Instant Run本身框架的程式碼,而沒有任何業務層的程式碼。

  3. 工程程式碼插樁——IncretmentalChange;這個插裝裡面會涉及到具體的IncretmentalChange類。

  4. 工程程式碼放入instantrun.zip;這裡的邏輯是當整個App執行起來之後才回去解壓這個包裡面的具體工程程式碼,執行整個業務邏輯。

  • Instant Run在第一次構建apk時,使用ASM在每一個方法中注入了類似如下的程式碼 (ASM 是一個 Java 位元組碼操控框架。它能被用來動態生成類或者增強既有類的功能)

//$change實現了IncrementalChange這個抽象介面。
//當點選InstantRun時,如果方法沒有變化則$change為null,就呼叫return,不做任何處理。
//如果方法有變化,就生成替換類,假設MainActivity的onCreate方法做了修改,就會生成替換類MainActivity$override,
//這個類實現了IncrementalChange介面,同時也會生成一個AppPatchesLoaderImpl類,這個類的getPatchedClasses方法
//會返回被修改的類的列表(裡面包含了MainActivity),根據列表會將MainActivity的$change設定為MainActivity$override
//因此滿足了localIncrementalChange != null,會執行MainActivity$override的access$dispatch方法,
//access$dispatch方法中會根據引數”onCreate.(Landroid/os/Bundle;)V”執行MainActivity$override的onCreate方法,
//從而實現了onCreate方法的修改。
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {//2
    localIncrementalChange.access$dispatch(
            "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                    paramBundle });
    return;
}

被廢棄的Instant Run

Android Studio 3.5 中一個顯著變化是引入了 Apply Changes,它取代了舊的 Instant Run。Instant Run 是為了更容易地對應用程式進行小的更改並測試它們,但它會產生一些問題。為了解決這一問題,谷歌已經徹底刪除了 Instant Run,並從根本上構建了 Apply Changes ,不再在構建過程中修改 APK,而是使用執行時工具動態地重新定義類,它應該比立刻執行更可靠和更快。

優點

  • 實時生效,不需要重新啟動

  • 支援增加方法和類

  • 支援方法級別的修復,包括靜態方法

  • 對每個產品程式碼的每個函式都在編譯打包階段自動的插入了一段程式碼,插入過程對業務開發是完全透明

缺點

  • 程式碼是侵入式的,會在原有的類中加入相關程式碼

  • 會增大apk的體積

資源修復:

  • 目前市面上大部分資源熱修復方案基本都參考了Instant Run的實現, 其主要分兩步:

  1. 建立新的AssetManager,並通過反射呼叫addAssetPath載入完整的新資源包;

  2. 找到所有之前引用到原有AssetManager的地方,通過反射,把引用處 替換為新AssetManager;

  • 這裡的具體原理可以參考章 探索Android開源框架 - 10. 外掛化原理 中的資源載入部分;

  • Sophix: 構造了一個package id為0x66的資源包(原有資源包為 0x7f),此包只包含改變了的資源項,然後直接在原有的AssetManager中 addAssetPath這個包就可以了,不修改AssetManager的引用處,替換更快更安全

so庫修復:

  • 主要是更新so,也就是重新載入so,主要用到了System的load和loadLibrary方法

  • System.load(""): 傳入so在磁碟的完整路徑,用於載入指定路徑的so

@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
  • System.loadLibrary(""):傳入so名稱,用於載入app安裝後自動從apk包中複製到/data/data/packagename/lib下的so

@CallerSensitive
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
  • 最終都會呼叫到LoadNativeLibrary(),其主要做了如下工作:

  1. 判斷so檔案是否已經載入,若已經載入判斷與class_Loader是否一樣,避免so重複載入;

  2. 如果so檔案沒有被載入,開啟so並得到so控制代碼,如果so控制代碼獲取失敗,就返回false,常見新的SharedLibrary,如果傳入path對應的library為空指標,就將建立的SharedLibrary賦值給library,並將library儲存到libraries_中;

  3. 查詢JNI_OnLoad的函式指標,根據不同情況設定was_successful的值,最終返回該was_successful;

兩種方案:

  1. 將so補丁插入到NativeLibraryElement陣列的前部,讓so補丁的路徑先被返回和載入;

  2. 呼叫System.load方法來接管so的載入入口;

參考

  • Android進階解密

  • Android熱修復技術原理詳解

  • Android 外掛化和熱修復知識梳理

  • 全面解析 Android 熱修復原理

  • 乾貨滿滿,Android熱修復方案介紹

  • 熱修復原理學習(6)資源熱修復技術

  • Android熱修復升級探索——SO庫修復方案

  • 深入探索Android熱修復技術原理(關注微信公眾號今陽說,回覆關鍵字"熱修復"領取書籍pdf)

我是今陽,如果想要進階和了解更多的乾貨,歡迎關注微信公眾號 “今陽說” 接收我的最新文章