前言
之前的文章里講到插件化最基礎(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