熱修復的三個部分
熱修復分為三個部分,分別是Java代碼部分熱修復
,Native代碼部分熱修復
,還有資源熱修復
。
資源部分熱更新直接反射更改所有保存的AssetManager和Resources對象就行(可能需要重啟應用)
Native代碼部分也很簡單,系統找到一個so文件的路徑是根據ClassLoader找的,修改ClassLoader里保存的路徑就行(可能需要重啟應用)
Java部分的話目前主流有兩種方式,一種是Java派,一種是Native派。
-
java派:通過修改ClassLoader來讓系統優先加載補丁包里的類
代表作有騰訊的tinker
,谷歌官方的Instant Run
,包括multidex
也是采用的這種方案
優點是穩定性較好,缺點是可能需要重啟應用 -
native派:通過內存操作實現,比如方法替換等
代表作是阿里的SopHix
,如果算上hook框架的話,還有dexposed,epic等等
優點是即時生效無需重啟,缺點是穩定性不好:
如果采用方法替換方式實現,假如這個方法被內聯/Sharpening優化了,那么就失效了;inline hook則無法修改超短方法。
熱修復后使用反射調用對應方法時可能發生IllegalArgumentException。
類加載方案
- 什么是 dex?
Android系統仿照java搞了一個虛擬機,不過它不叫JVM,它叫Dalvik/ART VM他們還是有很大區別的(這是不是我們的重點)。我們只需要知道,Dalvik/ART VM 虛擬機加載類和資源也是要用到ClassLoader,不過Jvm通過ClassLoader加載的class字節碼,而Dalvik/ART VM通過ClassLoader加載則是dex。 - ClassLoader的加載過程
ClassLoader的加載過程,其中一個環節就是調用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內部封裝了DexFile,DexFile用于加載dex文件,因此每個dex文件對應一個Element。
多個Element組成了有序的Element數組dexElements。當要查找類時,會先遍歷dex文件數組,然后調用Element的findClass方法,其方法內部會調用DexFile的loadClassBinaryName方法查找類。如果在Element中(dex文件)找到了該類就返回,如果沒有找到就接著在下一個Element中進行查找。
根據上面的查找流程,我們將有bug的類Key.class進行修改,再將Key.class打包成包含dex的補丁包Patch.jar,放在Element數組dexElements的第一個元素,這樣會首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class,排在數組后面的dex文件中的存在bug的Key.class根據ClassLoader的雙親委托模式就不會被加載,這就是類加載方案.
微信Tinker
將新舊apk做了diff,得到patch.dex,然后將patch.dex與手機中apk的classes.dex做合并,生成新的classes.dex,然后在運行時通過反射將classes.dex放在Element數組的第一個元素。
JavaHook 方案
美團 Robus 為代表
- 打基礎包時插樁,在每個方法前插入一段類型為 ChangeQuickRedirect 靜態變量的邏輯
- 加載補丁時,從補丁包中讀取要替換的類及具體替換的方法實現,新建 ClassLoader 加載補丁dex。
下面通過Robust的源碼來進行分析。 首先看一下打基礎包是插入的代碼邏輯,如下:
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
//為每個方法自動插入修復邏輯代碼,如果ChangeQuickRedirect為空則不執行
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的核心修復源碼如下:
public class PatchExecutor extends Thread {
@Override
public void run() {
...
applyPatchList(patches);
...
}
/**
* 應用補丁列表
*/
protected void applyPatchList(List<Patch> patches) {
...
for (Patch p : patches) {
...
currentPatchResult = patch(context, p);
...
}
}
/**
* 核心修復源碼
*/
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) {
...
}
...
//通過遍歷其中的類信息進而反射修改其中 ChangeQuickRedirect 對象的值
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;
}
}