代表:TInker,手机QQ空间、Nuwa
原理:将补丁Dex对象的DexFile对象注入到系统ClassLoader相关联的DexPathList对象的dexElements数组的最前面
代表:AndFix,阿里百川HotFix
原理:在Native层对方法的整体数据结构(Method/ArtMethod)进行替换
代表:Instant Run
原理:基于双亲委派机制,用自定义的IncrementalClassLoader加载补丁Dex,同时将该类加载器设置为系统类加载器的父加载器
ClassLoader的加载路径可以包含多个dex文件,每个dex文件关联一个Element,多个dex文件排列成一个有序的数组dexElements,当查找类的时候会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找到则返回,如果找不到从下一个dex文件继续查找。理论上如果在不同的dex中有相同的类存在,那么优先选择排在前面的dex文件的类。
我们解压一个apk,可以看到有这些.dex分包。
对应在代码里,BaseDexClassLoader中的pathList就是这些分包。
类加载器 ClassLoader
子类:BaseDexClassLoader
findClass是来寻找指定的类,我们可以看到,会通过pathList来寻找类。
pathList的类型是DexPathList ,我们再来看DexPathList,可以看到有一个dexElements属性,是Dex的集合。
再来看DexPathList的构造方法
可以看到,通过makePathElements
方法,对dexElements进行赋值。
来看makePathElements
方法
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
List<Element> elements = new ArrayList<>();
//遍历文件
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
//如果文件名包含 .dex 后缀
if (name.endsWith(DEX_SUFFIX)) {
try {
//尝试加载Dex文件
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
//将dex文件传入Element中,并将element添加到elements集合中
elements.add(new Element(dir, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
可以看到,这里会遍历目录下的所有文件,然后,如果后缀匹配.dex
,则尝试加载dex,如果加载成功,就将dex添加到elements集合中。
最终,将elements转为数组,并返回。
然后我们知道,App安装成功的时候,会复制一份apk文件到应用的私有目录
/data/app/packageName~1/base.apk
所以可以有思路:将修复好的x.dex下载到本机,插桩到dex集合的最前面,优先加载当中修复好的java类。
1.获取系统ClassLoader的pathList对象
Object pathList = Reflect.on(loader).field("pathList").get();
2.调用makePathElements构造补丁的dexElements
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
Object[] patchDexElements = makePathElements(pathList,extraDexFiles,FileUtil.getDexOptDir(context),suppressedExceptions);
3.将补丁Dex注入系统ClassLoader的pathList对象的dexElements的最前面
expandElementsArray(pathList,patchDexElements);
以上就是QQ空间超级补丁技术,Tinker和QQ空间超级补丁技术的原理基本相同,但对部分缺点进行了优化。
Tinker针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。
具体详见阿里最新热修复Sophix与QQ超级补丁和Tinker的实现与总结
dex插装可能会导致该异常,所以需要破坏这3个条件的其中一个。对于Tinker,就是破坏第三个条件,就是通过全量后的方式,以避免打上预校验标识的类,直接使类不在同一个dex中。