ClassLoader類加載流程補(bǔ)充

之前寫過一篇ClassLoader的筆記介紹了如何用ClassLoader去加載外部dex包,但是那個(gè)場景更多是插件化的場景,主要講的是雙親委托的流程。

最近的項(xiàng)目里面涉及到了一點(diǎn)熱修復(fù)的需求,如果用插件化的做法新增接口層或者改用反射調(diào)用代價(jià)比較大,更希望的時(shí)候可以用外部dex的類直接替換apk內(nèi)部的類。整個(gè)原理也比較簡單,這里先把之前漏講的findClass流程講一下。

findClass流程

安卓應(yīng)用啟動(dòng)后的默認(rèn)ClassLoader是PathClassLoader,而findClass方法實(shí)際是在父類BaseDexClassLoader里面定義的。

BaseDexClassLoader.findClass里面實(shí)際是調(diào)用DexPathList.findClass去加載的類:

// https://cs.android.com/android/platform/superproject/+/android-platform-13.0.0_r6:libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
private final DexPathList pathList;
...
protected Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c != null) {
        return c;
    }
    ...
}

而DexPathList.findClass則是遍歷dexElements去調(diào)用內(nèi)部類Element的findClass最終調(diào)用DexFile.loadClassBinaryName:

https://cs.android.com/android/platform/superproject/+/android-platform-13.0.0_r6:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
private Element[] dexElements;
...
public Class<?> findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }
    ...
}

static class Element {
    ...
    private final DexFile dexFile;
    ...
    public Class<?> findClass(String name, ClassLoader definingContext,
            List<Throwable> suppressed) {
        return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                : null;
    }
    ...
}

而DexFile.loadClassBinaryName最終會(huì)調(diào)用到DexFile.defineClassNative去到native層解析dex創(chuàng)建類:

// https://cs.android.com/android/platform/superproject/+/android-platform-13.0.0_r6:libcore/dalvik/src/main/java/dalvik/system/DexFile.java
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}
...
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
                                                  DexFile dexFile)
            throws ClassNotFoundException, NoClassDefFoundError;

可以大概總結(jié)為BaseDexClassLoader委托DexPathList去加載類,而DexPathList內(nèi)部有個(gè)Element數(shù)組,每個(gè)Element代表一個(gè)dex文件,DexPathList去加載類的原理則是遍歷Element數(shù)組,看類在哪個(gè)dex可以加載出來。

Tinker熱修復(fù)原理

知道了類加載的流程之后,熱修復(fù)的原理實(shí)際上也比較好理解: 用外部dex創(chuàng)建Element,插入到Element數(shù)組最前面。這樣的話在findClass的時(shí)候就會(huì)優(yōu)先加載外部dex的類,而不是apk內(nèi)部的類了。

不過這里還有個(gè)小問題,如何用外部dex創(chuàng)建Element?

答案是我們可以用DexClassLoader加載dex讓它幫我們生成Element,然后用反射獲取。

獲取到了之后也是比較順理成章的用反射插入到默認(rèn)的ClassLoader的pathList的Element數(shù)組最前面:

// 用DexClassLoader加載外部dex,并獲取Element數(shù)組
val dexClassLoader = DexClassLoader(dexFile.path, context.cacheDir.path, null, context.classLoader)
val newPathList = getDeclaredField(dexClassLoader, BaseDexClassLoader::class.java, "pathList")!!
val newDexElements = getDeclaredField(newPathList, "dalvik.system.DexPathList", "dexElements")!!

// 獲取進(jìn)程原本的Element數(shù)組
val oldPathList = getDeclaredField(context.classLoader, BaseDexClassLoader::class.java, "pathList")!!
val oldDexElements = getDeclaredField(oldPathList, "dalvik.system.DexPathList", "dexElements")!!

// 合并兩個(gè)Element數(shù)組,把DexClassLoader的Element數(shù)組放在前面
val combineArray = combineDexArray(newDexElements, oldDexElements)

// 修改進(jìn)程原本的Element數(shù)組為合并的新數(shù)組
setDeclaredField(oldPathList, "dalvik.system.DexPathList", "dexElements", combineArray)

完整的代碼已經(jīng)上傳到GitHub,demo里面DemoUtils.getString返回的是"this is a bug",而我修改成"bug fix"編譯出jar之后用dx工具轉(zhuǎn)換成hotfix.dex放到assets:

fun getString(): String {
    return "this is a bug"
    // return "bug fix"
}

在Application.onCreate里面加載這個(gè)dex:

val patch = File(cacheDir, HOTFIX_DEX)
assets.open(HOTFIX_DEX).use { src ->
    patch.outputStream().use { dest ->
        FileUtils.copy(src, dest)
    }
}
PatchLoader.loadPatch(this, patch)

最終在MainActivity里面讀取出來的就是修復(fù)后的"bug fix":

findViewById<TextView>(R.id.label).text = DemoUtils.getString()

Tinker的核心原理就是這樣的。不過這里還有個(gè)細(xì)節(jié)就是外部dex的加載是在Application里面執(zhí)行的,單如果需要修復(fù)Application的bug怎么辦?

它的解決方法是把Applcation的邏輯都挪到ApplicationLike里面,由Tinker加載完dex之后再在Application去調(diào)用ApplicationLike的生命周期回調(diào)。

其他熱修復(fù)方案

除了修改Element數(shù)組方案之外還有其他的熱修復(fù)方案可以參考下。

1.Robust:

使用插樁技術(shù)在每個(gè)類的每個(gè)方法最前面插入判斷代碼,如果有加載外部dex就反射執(zhí)行外部dex對(duì)應(yīng)的方法然后返回:

public class DemoClass {
    // 插樁生成
    public static ChangeQuickRedirect changeQuickRedirect;

    public int foo() {
        // 插樁生成
        if (changeQuickRedirect != null) {
            // 使用changeQuickRedirect去調(diào)用外部dex里面的DemoClass.foo方法
        }


        return 1;
    }
}

2.AndFix

從前面Tinker的原理我們可以看到類最終是由DexFile.defineClassNative在native層加載的,實(shí)際上java層的類和方法會(huì)對(duì)應(yīng)native層的一堆指針,阿里的AndFix就是直接在native層把舊類的指針直接替換成外部dex新類的指針。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容