热修复的三个部分
热修复分为三个部分,分别是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;
}
}