前言
之前的文章里講到插件化最基礎的內(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修復的類插到最前面,這樣就可以在執(zhí)行方法的時候替換掉有bug的類,完成熱修復。

實現(xiàn)
看核心代碼,根據(jù)上面的思路,可以通過DexClassLoader加載一個patch,并將這個ClassLoader中的Element數(shù)組取出放到PathClassLoader中的Element數(shù)組前面即可,代碼如下,變量中已dex開頭的為DexClassLoader相關實例,以path開頭的為PathClassLoader相關實例。
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, 將修復bug的classLoader放在最前面
Object merge = mergeDex(dexElements, pathElements);
//將合并后的pathList設置回去
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)修復后的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)通過熱修復的方式修復了先前的bug。
注意:在Android6.0之后的手機上,如果將patch放到了/sdcard中一定要申請讀權(quán)限,否則即使加載成功,已無法得到dex文件,造成patch失敗,開始的時候就踩了這個坑,明明ClassLoader加載成功了但是patch失敗。
小結(jié)
通過加載patch.pak的方式,并將Element插入到PathClassLoader中的Element最前面的方式可以進行熱修復,在實際上是可行的。
但是這樣有個缺點,就是要改一個類中的某個方法需要將整個類下發(fā),而且不能是Android的四大組件,使用起來有局限性。另一方面,加載類的時機不好確定,很難做到立即生效,時效性一般。
附上ClassLoader相關源碼的git地址: https://github.com/aosp-mirror/platform_dalvik.git