Android插件化實(shí)踐(3)--熱修復(fù)

前言

之前的文章里講到插件化最基礎(chǔ)的內(nèi)容,如何利用動態(tài)代碼和父委托機(jī)制實(shí)現(xiàn)activity的動態(tài)下發(fā)。這篇文章會講到如何利用ClassLoader進(jìn)行hotfix。

類加載過程

父委托機(jī)制

首先回顧一下父委托機(jī)制,在Java中查找類的過程是從父ClassLoader向子ClassLoader進(jìn)行的,具體參考Android插件化實(shí)踐(2),過程如下

那么在某個(gè)ClassLoader內(nèi)部是如何實(shí)現(xiàn)findClass的呢?看源碼,首先看BaseDexClassLoader(源碼位置libcore/dalvik/src/main/java/dalvik/system/),它是PathClassLoader的父類,在構(gòu)造方法中可以看到生成了一個(gè)DexPathList的實(shí)例,同樣傳入了dexPath。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

    if (reporter != null) {
        reportClassLoaderChain();
    }
}

接下來在BaseDexClassLoader的findClass()方法中可以看到調(diào)用了DexPathList中的findClass()方法,代碼如下

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException(
                "Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

接著來看DexPathList(源碼位置libcore/dalvik/src/main/java/dalvik/system/DexPathList.java),這里的findClass()方法又調(diào)用了Element中的findClass()方法,代碼如下

public Class<?> findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }

    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

再來看看dexElements的定義,是一個(gè)Element數(shù)組,這個(gè)類中儲存真正的dex文件(DexFile),具體內(nèi)容可以看代碼

private final Element[] dexElements;

最終又調(diào)用了Element中的findClass()方法,代碼如下

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

以上findClass的過程可以看下圖


最后一步中可以看到會遍歷Element數(shù)組,里面存儲著ClassLoader中的dexFile,而且是順序遍歷的。如果在類查找的過程中有機(jī)會把patch修復(fù)的類插到最前面,這樣就可以在執(zhí)行方法的時(shí)候替換掉有bug的類,完成熱修復(fù)。

實(shí)現(xiàn)

看核心代碼,根據(jù)上面的思路,可以通過DexClassLoader加載一個(gè)patch,并將這個(gè)ClassLoader中的Element數(shù)組取出放到PathClassLoader中的Element數(shù)組前面即可,代碼如下,變量中已dex開頭的為DexClassLoader相關(guān)實(shí)例,以path開頭的為PathClassLoader相關(guān)實(shí)例。

private static void mergePathList(Context context, String dexPath) {
    File optPath = context.getDir("dex", Context.MODE_PRIVATE);

    ClassLoader parent = context.getClassLoader();
    if (parent == null) {
        return;
    }

    //通過DexClassLoader加載apk
    DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
            optPath.getAbsolutePath(), null, parent);

    try {
        //獲取外部dex中的pathList
        Class<?> baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");

            //獲取dex中的pathList
        Object dexPathList = getField(dexClassLoader, baseDexClassLoader, "pathList");
        Object dexElements = getField(dexPathList, dexPathList.getClass(), "dexElements");

        //獲取本地apk中的pathList
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object pathPathList = getField(pathClassLoader, baseDexClassLoader, "pathList");
        Object pathElements = getField(pathPathList, pathPathList.getClass(), "dexElements");

        //合并pathList, 將修復(fù)bug的classLoader放在最前面
        Object merge = mergeDex(dexElements, pathElements);

        //將合并后的pathList設(shè)置回去
        Object pathList = getField(pathClassLoader, baseDexClassLoader, "pathList");
        setField(pathList, pathList.getClass(), "dexElements", merge);

        Log.d(TAG, "mergePathList: finish merge");

    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

}

測試

我們自定義一個(gè)對象MyString,代碼如下

public class MyString {

    private String str;

    public MyString(String str) {
        this.str = str;
    }

    public int getLength() {
        return str.length();
    }
}

在activity中加一個(gè)按鈕并實(shí)現(xiàn)onClick()方法,MyString傳入null,這里必掛,因?yàn)槌蓡T變量str沒有初始化

public void onCrashClick(View v) {
    MyString myString = new MyString(null);
    Log.d(TAG, "onCrashClick: " + myString.getLength());
}

接下來我們寫一個(gè)patch.apk,新加一個(gè)Android工程,里面只實(shí)現(xiàn)修復(fù)后的MyString,代碼如下

public class MyString {

    private String str;

    public MyString(String str) {
        this.str = str;
    }

    public int getLength() {
        return str == null ? 0 : str.length();
    }
}

將生成好的apk文件push到手機(jī)中,然后在啟動的時(shí)候進(jìn)行加載,然后再調(diào)用onCrashClick()的時(shí)候可以看到?jīng)]有crash,并且看到日志如下

12-27 14:39:17.947 3555-3555/com.test.hotfix D/MainActivity: onCrashClick: 0

至此,可以看到已經(jīng)通過熱修復(fù)的方式修復(fù)了先前的bug。

注意:在Android6.0之后的手機(jī)上,如果將patch放到了/sdcard中一定要申請讀權(quán)限,否則即使加載成功,已無法得到dex文件,造成patch失敗,開始的時(shí)候就踩了這個(gè)坑,明明ClassLoader加載成功了但是patch失敗。

小結(jié)

通過加載patch.pak的方式,并將Element插入到PathClassLoader中的Element最前面的方式可以進(jìn)行熱修復(fù),在實(shí)際上是可行的。

但是這樣有個(gè)缺點(diǎn),就是要改一個(gè)類中的某個(gè)方法需要將整個(gè)類下發(fā),而且不能是Android的四大組件,使用起來有局限性。另一方面,加載類的時(shí)機(jī)不好確定,很難做到立即生效,時(shí)效性一般。

附上ClassLoader相關(guān)源碼的git地址: https://github.com/aosp-mirror/platform_dalvik.git

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

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

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