Android熱修復Tinker原理分析

目錄

1、tinker的class文件修復
2、tinker的資源文件修復
3、幾種熱修復方案對比

1、tinker的class文件修復

1.1、先說dex文件的加載和類的查找過程

1.1.1、dex文件的加載過程

Java層通過我們會通過創建一個DexClassLoader來加載我們的dex,下面就以此爲切入點進行

dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());

//查看DexClassLoader的構造方法。
public class DexClassLoader extends BaseDexClassLoader {
    // dexPath:是加載apk/dex/jar的路徑
    // optimizedDirectory:是優化dex後得到的.odex文件的輸出路徑
    // libraryPath:是加載的時候需要用到的so庫
    // parent:給DexClassLoader指定父加載器
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

//可以看到它調用的是父類的構造函數,所以直接來看BaseDexClassLoader的構造函數。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

//創建了一個DexPathList實例,下面來看看DexPathList的構造函數。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
}

//它調用的是makeDexElements方法來創建一個Element數組來存放Element對象,每個Element對象包含一個DexFile對象。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();

        // 如果是一個dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            // Raw dex file (not inside a zip/jar).
            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException ex) {
                System.logE("Unable to load dex file: " + file, ex);
            }
        // 如果是一個apk或者jar或者zip文件
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            zip = file;

            try {
                // 1、調用loadDexFile加載dex文件,得到一個DexFile對象
                    loadDexFile通過c++層native方法去加載dex文件
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException suppressed) {
               
                suppressedExceptions.add(suppressed);
            }
        } else if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else {
            System.logW("Unknown file type for: " + file);
        }
        
        // 2、把DexFile對象封裝到Element對象中,然後將Element對象加入Element數組
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

dex文件的加載流程:我們會使用DexClassLoader去加載dex文件,DexClassLoader會將這個任務委派給DexPathList中的makeDexElements方法,在makeDexElements中調用了native層的 c++方法去真正的加載dex文件,然後返回DexFile的對象,通過這個對象構建一個Element的對象,然後將這個Element添加到dexElements的數組中。

1.1.2、class文件的查找過程

//DexClassLoader間接調用父類findClass方法,findClass方法中調用DexPathList中的DexPathList方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

//看DexPathList中的findClass方法,可以看到它是遍歷dexElements數組,到每個dex文件去尋找當前需要的類,找到之後直接返回不往下找了
    public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

類的查找過程:DexClassLoader通過findClass去查找一個類,同樣它也是委派給DexPathList的findClass去查找,在DexPathList的findClass中會去遍歷我們上面創建的dexElements數組,然後在每個dex中去查找相應的類,找到之後就返回,不再向後查找。

1.2、Tinker中class修復過程

1.2.1、先看tinker-補丁包合成流程圖

補丁包的合成流程:當tinker收到補丁包bug path後,它會開啓一個service,和當前有問題的bug dex和成爲一個新的fixed dex文件,然後置於tinker dex文件加載路徑。

1.2.2、再看tinker-合成後的補丁包加載流程

補丁包的加載流程:獲取到fixed dex文件後,通過反射DexPathList中的dexElements數組,將fixed dex插入到dexElements數組的最前面。classloader在尋找bug class的時候,找到的就是最前面的dex文件中我們已修復的fixed class。

2、tinker的資源文件修復

2.1、Context.getResources()

我們訪問資源的時候通過Context.getResources(),獲取Resources對象,然後通過Resources對象就可以訪問各種資源了。
Context.getResources()流程
Context.getResources()獲取的ContextImpl中的mResources對象。

mResources對象的獲取過程

mResources是在activitythread初始化的時候獲取的,具體是通過
ResourcesManager.getInstance().getResources()去得到一個Resources。

這個方法的思想是這樣的:在ResourcesManager中,所有的資源對象都被存儲在ArrayMap中,首先根據當前的請求參數去查找資源,如果找到了就返回,否則就創建一個Resources對象返回,並且放到ArrayMap中。

爲什麼會有多個資源對象,因爲不同分辨率、不同系統版本所對應的資源文件可以不同,比如摺疊屏手機就有兩個不同分辨率的屏幕對應,drawable-hdpi和drawable-xhdpi。

Resources對象的創建過程

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)

 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();
        if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }

創建一個resource需要一個AssetManager, 然後通過AssetManager的addAssetPath去加載資源文件。
然後我們就可以通過resource訪問各種資源文件了。

2.2、tinker的資源文件修復

 public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
   
final Field[] packagesFields;
        if (Build.VERSION.SDK_INT < 27) {
            packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
        } else {
            packagesFields = new Field[]{packagesFiled};
        }
//首先將activityThread中所有LoadApk中的resDir的值替換成新合成的資源文件路徑
        for (Field field : packagesFields) {
            final Object value = field.get(currentActivityThread);

            for (Map.Entry<String, WeakReference<?>> entry
                    : ((Map<String, WeakReference<?>>) value).entrySet()) {
                final Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                final String resDirPath = (String) resDir.get(loadedApk);
                if (appInfo.sourceDir.equals(resDirPath)) {
                    resDir.set(loadedApk, externalResourceFile);
                }
            }
        }

// Create a new AssetManager instance 
 //創建一個新的AssetManager
        newAssetManager = (AssetManager) findConstructor(assets).newInstance();

// 並把資源補丁apk加載進新的 AssetManager 中
        if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }

//循環替換ResourcesManager中所有Resources對象的AssetManager。
        for (WeakReference<Resources> wr : references) {
            final Resources resources = wr.get();
            if (resources == null) {
                continue;
            }
            try {
        // 把原來 resources 的 mAssets 屬性替換成新的 AssetManager 對象
                assetsFiled.set(resources, newAssetManager);
            } catch (Throwable ignore) {
            }

            clearPreloadTypedArrayIssue(resources);
    // 最後調用 updateConfiguration 方法來確保資源更新了
            resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
        }

1、首先將activityThread中所有LoadApk中的resDir的值替換成新合成的資源文件路徑(獲取Resources時,會以LoadApk中的resDir作爲key去ResourcesManager中獲取)
2、創建一個新的AssetManager,並把資源補丁apk加載進新的 AssetManager 中
3、將ResourcesManager中所有Resources對象中AssetManager替換成我們新建的AssetManager,那麼所有的Resources對象獲取到的都是新合成的資源文件。

3、幾種熱修復方案對比

Andfix、QQ控件熱修復、Tinker。

3.1、Andfix

aandfix功能比較單一,只能修復方法,大致原理就是在運行時通過在native層去將bug方法替換成修復的方法。這樣導致了兩個問題:1、由於運行時,class已經加載,其field數值無法改變(要是能改變,通過這個class創建的對象就失效了),故其不能增加或者減少成員變量;2、因爲是動態的,跳過了類的初始化,所以對於靜態方法、靜態成員變量、構造方法處理可能會有問題,另外增加類也是不可能的;3、由於其採用在native層去進行方法替換,不同廠商手機可能對native層代碼做修改,故其兼容性可能較差。基於以上3個缺點,由於我們的app對兼容性要求較高,且可能對方法之外的地方做修改,所以Andfix是不滿足我們的需求的。

andfix修復方法的前提是對bug方法修復之後使用註解進行標記(bug類名和bug方法名),然後將這個補丁文件記錄在一個patch.mf的文件中。

3.2、QQ空間的熱修復方案

QQ空間的熱修復方案和Tinker方案較爲類似,都是通過操作dexElements數組替換有問題的class來實現的,不同的是Tinker時將差分包和有問題的dex文件合成一個新的 fix dex插入到dexElements數組前面的,而QQ空間的熱修復方案是直接將差分包,插入到dexElements的前面。會導致引用類和直接被引用類不在同一個dex的錯誤:

dex文件轉化成odex文件期間會經歷兩個階段:pre_verified和optimize,pre_verified會判斷一個類的直接引用類是否和它在同一個dex文件,如果是,這個類就會被打上CLASS_ISPREVERIFIED,在optimize會根據這個標記去對這個類做指令優化。但是在類加載的時候,也會去判斷這個類和其直接引用類是否在同一個dex,如果不在則報錯。
如果一個直接被引用類是我們修復的方法,按照qq空間的修復方案,它和其引用類就會處於不同的dex文件,那麼類加載的時候就會報錯。

dex文件在dalvik執行之前會轉化成odex文件,在這個過程中會對dex中每個類做一個檢驗:如果引用類(A)的直接引用的類(B)和這個類(A)在同一個dex,這個引用類(A)會被打上CLASS_ISPREVERIFIED標記,在類加載的時候,就會去判斷類(A)和被引用類是否在一個dex,不在,則報錯。
如果這個被引用類(B)是我們修復的方法,它就會和其引用類(A)處於不同的dex文件,那麼類加載的時候就會報錯。

QQ空間的熱修復方案的解決方法是,使用aop在每個類的構造函數,引用了一個特殊的類(C),在編譯的時候將這個類打進一個單獨dex文件,這樣所有的類在dex轉化成odex的過程中都不會被打上標記,這樣就避免了上述錯誤的發生。

但是這樣有一個問題,如果在dex轉化成odex期間,不做pre_verified和optimize兩步,那麼這這兩步將推遲到類加載的階段,會拖慢類加載的速度。
另外採用javassist進行字節碼操作速度慢,會拖慢編譯速度。
故淘汰了QQ空間的熱修復方案。

3.3、Tinker3.4、tinker的其他優點

1、Tinker的覆蓋比較全面,對class、資源文件、so文件都支持,
2、另外它比較容易擴展,在修復的各個階段都提供了listener,我們可以重寫這個listener,去監聽修復的各個階段。

3.4、Tinker的缺點

它的問題在於
1、由於Tinker採用的將fix dex 插入到dexElements最前面的方式去修復bug,所有它需要重啓,不能即時生效。
2、Tinker在下發path的時候需要啓動一個service去將path和有問題的dex文件合成一個新的dex文件,這個過程是對性能影響較大的,但是合成只需要一次,後續就不需要了,這樣的性能損耗還是可以接受的;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章