Android進階(八)熱修復基本原理

1、代碼修復

一、類加載方案

(1)Dex分包原理

單個Dex文件裏面方法數不能超過65536個方法。java

(1)緣由:
由於android會把每個類的方法id檢索起來,存在一個鏈表結構裏面。可是這個鏈表的長度是用一個short類型來保存的, short佔兩個字節(保存-2的15次方到2的15次方-1,即-32768~32767),最大保存的數量就是65536。android

(2)解決方案:數組

  • 精簡方法數量,刪除沒用到的類、方法、第三方庫。
  • 使用ProGuard去掉一些未使用的代碼
  • 對部分模塊採用本地插件化的方式。
  • 分割Dex

Dex分包方案主要作的是在打包時將應用代碼分紅多個Dex,將應用啓動時必須用到的類和這些類的直接引用類放到主Dex中,其餘代碼放到次Dex中。當應用啓動時先加載主Dex,等到應用啓動後再動態的加載次Dex。bash

(2)類加載修復方案

若是Key.Class文件中存在異常,將該Class文件修復後,將其打入Patch.dex的補丁包
(1) 方案一:
經過反射獲取到PathClassLoader中的DexPathList,而後再拿到 DexPathList中的Element數組,將Patch.dex放在Element數組dexElements的第一個元素,最後將數組進行合併後並從新設置回去。在進行類加載的時候,因爲ClassLoader的雙親委託機制,該類只被加載一次,也就是說Patch.dex中的Key.Class會被加載。 微信

(2)方案二:
提供dex差量包patch.dex,將patch.dex與應用的classes.dex合併成一個完整的dex,完整dex加載後獲得dexFile對象,做爲參數構建一個Element對象,而後總體替換掉舊的dex-Elements數組。(Tinker)

(3)類加載方案的限制

方案一:框架

  • 因爲類是沒法進行卸載,因此類若是須要從新加載,則須要重啓App,因此類加載修復方案不是即時生效的。
  • 在ART模式下,若是類修改告終構,就會出現內存錯亂的問題。爲了解決這個問題,就必須把全部相關的調用類、父類子類等等所有加載到patch.dex中,致使補丁包大,耗時嚴重。

方案二:性能

  • 下次啓動修復
  • dex合併內存消耗可能致使OOM,最終dex合併失敗

二、底層替換方案

(1)基本方案

主要是在Native層替換原有方法,ArtMethod結構體中包含了Java方法的全部信息,包括執行入口、訪問權限、所屬類和代碼執行地址等。替換ArtMethod結構體中的字段或者替換整個ArtMethod結構體,就是底層替換方案。因爲直接替換了方法,能夠當即生效不須要重啓。gradle

(2)優缺點

(1)缺點ui

  • 不可以增減原有類的方法和字段,若是咱們增長了方法數,那麼方法索引數也會增長,這樣訪問方法時會沒法經過索引找到正確的方法。
  • 平臺兼容性問題,若是廠商對ArtMethod結構體進行了修改,替換機制就有問題。

(2)優勢this

  • Bug修復的即時性
  • 生成的PATCH體積小,性能影響低

2、資源修復

一、Instant Run

核心代碼:runtime/MonkeyPatcher.java

#MonkeyPatcher
public static void monkeyPatchExistingResources(@Nullable Context context,
                                                @Nullable String externalResourceFile,
                                                @Nullable Collection<Activity> activities) {
    ......                                        
    try {
        // Create a new AssetManager instance and point it to the resources installed under
        // (1)經過反射建立了一個newAssetManager,調用addAssetPath添加了sdcard上的資源包
        AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
        mAddAssetPath.setAccessible(true);
        if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }
        // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
        // in L, so we do it unconditionally.
        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
        mEnsureStringBlocks.setAccessible(true);
        mEnsureStringBlocks.invoke(newAssetManager);
        if (activities != null) {
            //(2)反射獲取Activity中AssetManager的引用,替換成新建立的newAssetManager
            for (Activity activity : activities) {
                Resources resources = activity.getResources();
                try {
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources);
                    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }
                Resources.Theme theme = activity.getTheme();
                try {
                    try {
                        Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                        ma.setAccessible(true);
                        ma.set(theme, newAssetManager);
                    } catch (NoSuchFieldException ignore) {
                        Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                        themeField.setAccessible(true);
                        Object impl = themeField.get(theme);
                        Field ma = impl.getClass().getDeclaredField("mAssets");
                        ma.setAccessible(true);
                        ma.set(impl, newAssetManager);
                    }
                ......
        }
        //(3)遍歷Resource弱引用的集合,將AssetManager替換成newAssetManager
        for (WeakReference<Resources> wr : references) {
            Resources resources = wr.get();
            if (resources != null) {
                // Set the AssetManager of the Resources instance to our brand new one
                try {
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources);
                    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }
                resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
            }
        }
    } catch (Throwable e) {
        throw new IllegalStateException(e);
    }
}
複製代碼
  • 反射構建新的AssetManager,並反射調用addAssertPath加載sdcard中的新資源包,這樣就獲得一個含有全部新資源的AssetManager
  • 將原來引用到AssetManager的地方,經過反射把引用處替換爲新的AssetManager

二、資源包替換(Sophix)

默認由Android SDK編譯出來的apk,其資源包的package id爲0x7f。framework-res.jar的資源package id爲0x01

  • 構造一個package id爲0x66的資源包(非0x7f和0x01),只包含已經改變的資源項。
  • 因爲不與已經加載的Ox7f衝突,因此能夠經過原有的AssetManager的addAssetPath加載這個包。

3、SO庫修復

本質是對native方法的修復和替換

一、so庫加載

(1)經過如下方法加載so庫

#System
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
參數爲so庫名稱,位於apk的lib目錄下

public static void load(String filename) {
    Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
}
加載外部自定義so庫文件,參數爲so庫在磁盤中的完整路徑
複製代碼
private static native String nativeLoad(String filename, ClassLoader loader, String librarySearchPath);
複製代碼

最終都是調用了native方法nativeLoad,參數fileName爲so在磁盤中的完整路徑名

(2)遍歷nativeLibraryDirectories目錄

#DexPathList
public String findLibrary(String libraryName) {
    String fileName = System.mapLibraryName(libraryName);
    for (File directory : nativeLibraryDirectories) {
        File file = new File(directory, fileName);
        if (file.exists() && file.isFile() && file.canRead()) {
            return file.getPath();
        }
    }
    return null;
}
複製代碼

相似於類加載的findClass方法,在數組中每個元素對應一個so庫,最終返回了so的路徑。若是將so補丁添加到數組的最前面,在調用方法加載so庫時,會先將補丁so的路徑返回。

二、SO修復方案

(1)接口替換

提供方法替代System.loadLibrary方法

  • 若是存在補丁so,則加載補丁so庫,不去加載apk安裝目錄下的so庫
  • 若是不存在補丁so,調用System.loadLibrary去加載安裝apk目錄下的so庫

(2)反射注入

由於加載so庫會遍歷nativeLibraryDirectories

  • 經過反射將補丁so庫的路徑插入到nativeLibraryDirectories數組的最前面
  • 遍歷nativeLibraryDirectories時,就會將補丁so庫進行返回並加載,從而達到修復目的

4、熱修復框架分析

  • 底層替換方案:阿里的AndFix、HotFix
  • 類加載方案:QQ空間補丁技術、微信的Tinker方案、餓了麼的Amigo
  • 兩者結合:Sophix

參考資料:

相關文章
相關標籤/搜索