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

前言

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

類加載過程

父委托機制

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

那么在某個ClassLoader內(nèi)部是如何實現(xiàn)findClass的呢遭赂?看源碼循诉,首先看BaseDexClassLoader(源碼位置libcore/dalvik/src/main/java/dalvik/system/),它是PathClassLoader的父類撇他,在構(gòu)造方法中可以看到生成了一個DexPathList的實例茄猫,同樣傳入了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的定義僻弹,是一個Element數(shù)組阿浓,這個類中儲存真正的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,而且是順序遍歷的卸耘。如果在類查找的過程中有機會把patch修復(fù)的類插到最前面退敦,這樣就可以在執(zhí)行方法的時候替換掉有bug的類,完成熱修復(fù)蚣抗。

實現(xiàn)

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

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();
    }

}

測試

我們自定義一個對象MyString,代碼如下

public class MyString {

    private String str;

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

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

在activity中加一個按鈕并實現(xiàn)onClick()方法迷捧,MyString傳入null织咧,這里必掛胀葱,因為成員變量str沒有初始化

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

接下來我們寫一個patch.apk,新加一個Android工程笙蒙,里面只實現(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到手機中,然后在啟動的時候進行加載捅位,然后再調(diào)用onCrashClick()的時候可以看到?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之后的手機上朝群,如果將patch放到了/sdcard中一定要申請讀權(quán)限,否則即使加載成功中符,已無法得到dex文件,造成patch失敗誉帅,開始的時候就踩了這個坑淀散,明明ClassLoader加載成功了但是patch失敗。

小結(jié)

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

但是這樣有個缺點亚再,就是要改一個類中的某個方法需要將整個類下發(fā)郭膛,而且不能是Android的四大組件,使用起來有局限性氛悬。另一方面则剃,加載類的時機不好確定,很難做到立即生效如捅,時效性一般棍现。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市镜遣,隨后出現(xiàn)的幾起案子己肮,更是在濱河造成了極大的恐慌,老刑警劉巖悲关,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谎僻,死亡現(xiàn)場離奇詭異,居然都是意外死亡寓辱,警方通過查閱死者的電腦和手機艘绍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讶舰,“玉大人鞍盗,你說我怎么就攤上這事需了。” “怎么了般甲?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵肋乍,是天一觀的道長。 經(jīng)常有香客問我敷存,道長墓造,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任锚烦,我火速辦了婚禮觅闽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘涮俄。我一直安慰自己蛉拙,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布彻亲。 她就那樣靜靜地躺著孕锄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪苞尝。 梳的紋絲不亂的頭發(fā)上畸肆,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音宙址,去河邊找鬼轴脐。 笑死,一個胖子當(dāng)著我的面吹牛抡砂,可吹牛的內(nèi)容都是我干的大咱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼舀患,長吁一口氣:“原來是場噩夢啊……” “哼徽级!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起聊浅,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤餐抢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后低匙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旷痕,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年顽冶,在試婚紗的時候發(fā)現(xiàn)自己被綠了欺抗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡强重,死狀恐怖绞呈,靈堂內(nèi)的尸體忽然破棺而出贸人,到底是詐尸還是另有隱情,我是刑警寧澤佃声,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布艺智,位于F島的核電站,受9級特大地震影響圾亏,放射性物質(zhì)發(fā)生泄漏十拣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一志鹃、第九天 我趴在偏房一處隱蔽的房頂上張望夭问。 院中可真熱鬧,春花似錦曹铃、人聲如沸缰趋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽埠胖。三九已至,卻和暖如春淳玩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背非竿。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工蜕着, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人红柱。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓承匣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锤悄。 傳聞我的和親對象是個殘疾皇子韧骗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容