熱修復(fù)的三個(gè)部分
熱修復(fù)分為三個(gè)部分军熏,分別是Java代碼部分熱修復(fù)
,Native代碼部分熱修復(fù)
,還有資源熱修復(fù)
歼争。
資源部分熱更新直接反射更改所有保存的AssetManager和Resources對(duì)象就行(可能需要重啟應(yīng)用)
Native代碼部分也很簡(jiǎn)單响迂,系統(tǒng)找到一個(gè)so文件的路徑是根據(jù)ClassLoader找的考抄,修改ClassLoader里保存的路徑就行(可能需要重啟應(yīng)用)
Java部分的話目前主流有兩種方式,一種是Java派栓拜,一種是Native派座泳。
-
java派:通過修改ClassLoader來讓系統(tǒng)優(yōu)先加載補(bǔ)丁包里的類
代表作有騰訊的tinker
惠昔,谷歌官方的Instant Run
,包括multidex
也是采用的這種方案
優(yōu)點(diǎn)是穩(wěn)定性較好挑势,缺點(diǎn)是可能需要重啟應(yīng)用 -
native派:通過內(nèi)存操作實(shí)現(xiàn)镇防,比如方法替換等
代表作是阿里的SopHix
,如果算上hook框架的話潮饱,還有dexposed来氧,epic等等
優(yōu)點(diǎn)是即時(shí)生效無需重啟,缺點(diǎn)是穩(wěn)定性不好:
如果采用方法替換方式實(shí)現(xiàn)香拉,假如這個(gè)方法被內(nèi)聯(lián)/Sharpening優(yōu)化了啦扬,那么就失效了;inline hook則無法修改超短方法凫碌。
熱修復(fù)后使用反射調(diào)用對(duì)應(yīng)方法時(shí)可能發(fā)生IllegalArgumentException扑毡。
類加載方案
- 什么是 dex?
Android系統(tǒng)仿照java搞了一個(gè)虛擬機(jī),不過它不叫JVM盛险,它叫Dalvik/ART VM他們還是有很大區(qū)別的(這是不是我們的重點(diǎn))瞄摊。我們只需要知道,Dalvik/ART VM 虛擬機(jī)加載類和資源也是要用到ClassLoader苦掘,不過Jvm通過ClassLoader加載的class字節(jié)碼换帜,而Dalvik/ART VM通過ClassLoader加載則是dex。 - ClassLoader的加載過程
ClassLoader的加載過程,其中一個(gè)環(huán)節(jié)就是調(diào)用DexPathList的findClass的方法鹤啡,如下所示:
libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element內(nèi)部封裝了DexFile惯驼,DexFile用于加載dex文件,因此每個(gè)dex文件對(duì)應(yīng)一個(gè)Element递瑰。
多個(gè)Element組成了有序的Element數(shù)組dexElements祟牲。當(dāng)要查找類時(shí),會(huì)先遍歷dex文件數(shù)組泣矛,然后調(diào)用Element的findClass方法疲眷,其方法內(nèi)部會(huì)調(diào)用DexFile的loadClassBinaryName方法查找類。如果在Element中(dex文件)找到了該類就返回您朽,如果沒有找到就接著在下一個(gè)Element中進(jìn)行查找狂丝。
根據(jù)上面的查找流程,我們將有bug的類Key.class進(jìn)行修改哗总,再將Key.class打包成包含dex的補(bǔ)丁包Patch.jar几颜,放在Element數(shù)組dexElements的第一個(gè)元素,這樣會(huì)首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class讯屈,排在數(shù)組后面的dex文件中的存在bug的Key.class根據(jù)ClassLoader的雙親委托模式就不會(huì)被加載蛋哭,這就是類加載方案.
微信Tinker
將新舊apk做了diff,得到patch.dex涮母,然后將patch.dex與手機(jī)中apk的classes.dex做合并谆趾,生成新的classes.dex躁愿,然后在運(yùn)行時(shí)通過反射將classes.dex放在Element數(shù)組的第一個(gè)元素。
JavaHook 方案
美團(tuán) Robus 為代表
- 打基礎(chǔ)包時(shí)插樁沪蓬,在每個(gè)方法前插入一段類型為 ChangeQuickRedirect 靜態(tài)變量的邏輯
- 加載補(bǔ)丁時(shí)彤钟,從補(bǔ)丁包中讀取要替換的類及具體替換的方法實(shí)現(xiàn),新建 ClassLoader 加載補(bǔ)丁dex跷叉。
下面通過Robust的源碼來進(jìn)行分析逸雹。 首先看一下打基礎(chǔ)包是插入的代碼邏輯,如下:
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
//為每個(gè)方法自動(dòng)插入修復(fù)邏輯代碼云挟,如果ChangeQuickRedirect為空則不執(zhí)行
if (u != null) {
if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
return;
}
}
super.onCreate(bundle);
...
}
Robust的核心修復(fù)源碼如下:
public class PatchExecutor extends Thread {
@Override
public void run() {
...
applyPatchList(patches);
...
}
/**
* 應(yīng)用補(bǔ)丁列表
*/
protected void applyPatchList(List<Patch> patches) {
...
for (Patch p : patches) {
...
currentPatchResult = patch(context, p);
...
}
}
/**
* 核心修復(fù)源碼
*/
protected boolean patch(Context context, Patch patch) {
...
//新建ClassLoader
DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
patch.delete(patch.getTempPath());
...
try {
patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
} catch (Throwable t) {
...
}
...
//通過遍歷其中的類信息進(jìn)而反射修改其中 ChangeQuickRedirect 對(duì)象的值
for (PatchedClassInfo patchedClassInfo : patchedClasses) {
...
try {
oldClass = classLoader.loadClass(patchedClassName.trim());
Field[] fields = oldClass.getDeclaredFields();
for (Field field : fields) {
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
changeQuickRedirectField = field;
break;
}
}
...
try {
patchClass = classLoader.loadClass(patchClassName);
Object patchObject = patchClass.newInstance();
changeQuickRedirectField.setAccessible(true);
changeQuickRedirectField.set(null, patchObject);
} catch (Throwable t) {
...
}
} catch (Throwable t) {
...
}
}
return true;
}
}