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