之前寫過一篇ClassLoader的筆記介紹了如何用ClassLoader去加載外部dex包,但是那個(gè)場景更多是插件化的場景,主要講的是雙親委托的流程。
最近的項(xiàng)目里面涉及到了一點(diǎn)熱修復(fù)的需求,如果用插件化的做法新增接口層或者改用反射調(diào)用代價(jià)比較大,更希望的時(shí)候可以用外部dex的類直接替換apk內(nèi)部的類。整個(gè)原理也比較簡單,這里先把之前漏講的findClass流程講一下。
findClass流程
安卓應(yīng)用啟動(dòng)后的默認(rèn)ClassLoader是PathClassLoader,而findClass方法實(shí)際是在父類BaseDexClassLoader里面定義的。
BaseDexClassLoader.findClass里面實(shí)際是調(diào)用DexPathList.findClass去加載的類:
// https://cs.android.com/android/platform/superproject/+/android-platform-13.0.0_r6:libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
private final DexPathList pathList;
...
protected Class<?> findClass(String name) throws ClassNotFoundException {
...
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c != null) {
return c;
}
...
}
而DexPathList.findClass則是遍歷dexElements去調(diào)用內(nèi)部類Element的findClass最終調(diào)用DexFile.loadClassBinaryName:
https://cs.android.com/android/platform/superproject/+/android-platform-13.0.0_r6:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
private Element[] dexElements;
...
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
...
}
static class Element {
...
private final DexFile dexFile;
...
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
...
}
而DexFile.loadClassBinaryName最終會(huì)調(diào)用到DexFile.defineClassNative去到native層解析dex創(chuàng)建類:
// https://cs.android.com/android/platform/superproject/+/android-platform-13.0.0_r6:libcore/dalvik/src/main/java/dalvik/system/DexFile.java
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
...
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile)
throws ClassNotFoundException, NoClassDefFoundError;
可以大概總結(jié)為BaseDexClassLoader委托DexPathList去加載類,而DexPathList內(nèi)部有個(gè)Element數(shù)組,每個(gè)Element代表一個(gè)dex文件,DexPathList去加載類的原理則是遍歷Element數(shù)組,看類在哪個(gè)dex可以加載出來。
Tinker熱修復(fù)原理
知道了類加載的流程之后,熱修復(fù)的原理實(shí)際上也比較好理解: 用外部dex創(chuàng)建Element,插入到Element數(shù)組最前面。這樣的話在findClass的時(shí)候就會(huì)優(yōu)先加載外部dex的類,而不是apk內(nèi)部的類了。
不過這里還有個(gè)小問題,如何用外部dex創(chuàng)建Element?
答案是我們可以用DexClassLoader加載dex讓它幫我們生成Element,然后用反射獲取。
獲取到了之后也是比較順理成章的用反射插入到默認(rèn)的ClassLoader的pathList的Element數(shù)組最前面:
// 用DexClassLoader加載外部dex,并獲取Element數(shù)組
val dexClassLoader = DexClassLoader(dexFile.path, context.cacheDir.path, null, context.classLoader)
val newPathList = getDeclaredField(dexClassLoader, BaseDexClassLoader::class.java, "pathList")!!
val newDexElements = getDeclaredField(newPathList, "dalvik.system.DexPathList", "dexElements")!!
// 獲取進(jìn)程原本的Element數(shù)組
val oldPathList = getDeclaredField(context.classLoader, BaseDexClassLoader::class.java, "pathList")!!
val oldDexElements = getDeclaredField(oldPathList, "dalvik.system.DexPathList", "dexElements")!!
// 合并兩個(gè)Element數(shù)組,把DexClassLoader的Element數(shù)組放在前面
val combineArray = combineDexArray(newDexElements, oldDexElements)
// 修改進(jìn)程原本的Element數(shù)組為合并的新數(shù)組
setDeclaredField(oldPathList, "dalvik.system.DexPathList", "dexElements", combineArray)
完整的代碼已經(jīng)上傳到GitHub,demo里面DemoUtils.getString返回的是"this is a bug",而我修改成"bug fix"編譯出jar之后用dx工具轉(zhuǎn)換成hotfix.dex放到assets:
fun getString(): String {
return "this is a bug"
// return "bug fix"
}
在Application.onCreate里面加載這個(gè)dex:
val patch = File(cacheDir, HOTFIX_DEX)
assets.open(HOTFIX_DEX).use { src ->
patch.outputStream().use { dest ->
FileUtils.copy(src, dest)
}
}
PatchLoader.loadPatch(this, patch)
最終在MainActivity里面讀取出來的就是修復(fù)后的"bug fix":
findViewById<TextView>(R.id.label).text = DemoUtils.getString()
Tinker的核心原理就是這樣的。不過這里還有個(gè)細(xì)節(jié)就是外部dex的加載是在Application里面執(zhí)行的,單如果需要修復(fù)Application的bug怎么辦?
它的解決方法是把Applcation的邏輯都挪到ApplicationLike里面,由Tinker加載完dex之后再在Application去調(diào)用ApplicationLike的生命周期回調(diào)。
其他熱修復(fù)方案
除了修改Element數(shù)組方案之外還有其他的熱修復(fù)方案可以參考下。
1.Robust:
使用插樁技術(shù)在每個(gè)類的每個(gè)方法最前面插入判斷代碼,如果有加載外部dex就反射執(zhí)行外部dex對(duì)應(yīng)的方法然后返回:
public class DemoClass {
// 插樁生成
public static ChangeQuickRedirect changeQuickRedirect;
public int foo() {
// 插樁生成
if (changeQuickRedirect != null) {
// 使用changeQuickRedirect去調(diào)用外部dex里面的DemoClass.foo方法
}
return 1;
}
}
2.AndFix
從前面Tinker的原理我們可以看到類最終是由DexFile.defineClassNative在native層加載的,實(shí)際上java層的類和方法會(huì)對(duì)應(yīng)native層的一堆指針,阿里的AndFix就是直接在native層把舊類的指針直接替換成外部dex新類的指針。