1,热修复方案:
(1) 阿里的AndFix 补丁方案,通过natvie层hook住带有bug的方法从而替换Java层的代码 。四五年没有维护了,应该是被弃用了,因为是通过C++代码来完成类的替换。实际就是反射拿到旧的类class,然后再把补丁包里面的class进行一个赋值,例如 old.class = new.class. 是可以立即生效的。
(2) 美团的Robust 补丁方案 ,就是在编译打包的阶段对每个函数都加入修复的代码。有点类似代理,将方法执行的代码重定向到修复了的方法中去。(运用了字节码插桩技术) 是可以立即生效的。
(3) 腾讯的Tinker, 通过对比指定的base APK版本 和修复好了之后的 APK包中dex文件 , 补丁包就是两者的差分文件,patch.dex , 再把patch.dex 和用户手机里的旧的classes.dex文件合成得到修复之后的dex文件,然后重启加载合成新的dex文件 (有点像增量更新)。
(4) 美团的Qzone超级补丁方案,基于dex分包方案,把BUG方法修复以后,放到一个单独的dex补丁文件,让程序运行期间加载dex补丁,执行修复后的方法。
2:类加载机制
不管是哪一种热修复方案,都用到了类加载机制与反射。首先类加载机制的一个核心就是,双亲委托机制(双亲有歧义实际上我感觉应该叫父亲委托机制),意思是某个类加载器在加载类时,首先将加载任务委托给父 - 类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。我们还是用Android SDK 的源码来看一看,看一下ClassLoader.java里面的loadClass方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
// 首先判断这个类是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 这个parent实际上就是ClassLoader的一个成员变量,一般是构造子类时来给它赋值
c = parent.loadClass(name, false);
} else {
//如果子类没有给它赋值,直接调用BootClassLoader来找这个类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//如果父类加载器也找不到,通过自己来找
c = findClass(name);
}
}
return c;
}
我们主要看看这个findClass, 热修复我们一般拿到的类加载器是PathClassLoader。
findClass这个函数在PathClassLoader 找不到,说明在它的父类里面BaseDexClassLoader里面。那么我再来看一看BaseDexClassLoader这个类里面的findClass方法。
可以看到这个成员变量pathList是一个对象,对应的类是DexPathList, findClass实际上是调用DexPathList里面的findClass,我们再继续看下源码。
DexPathList这个类的findClass方法,是通过遍历dexElements这个数组,调用数组里面的元素element的findClass方法。那么这个dexElements又是个什么东西呢?
就是DexPathList的一个成员变量 ,那么是在那里赋值的呢?继续找一下源码。
可以看到是通过maxDexElements这个函数来赋值的,首先注意第一个参数dexPath ,这个实际上就是传入的我们APK里面编译打包之后的Classes.dex 文件的路径,所以可以知道makeDexElements这个函数的功能就是把DexFile转化成Element数组的。(注意: 有的是版本是通过makePathElements这个函数来吧DexFile文件转化成数组的,不同的版本可能参数会有变化,我这个是Andoid9.0的源码)
可以看到这里面有一个for循环,遍历那个Dex File ,当我们APP的方法超过了65535个方法之后,我们一般都会进行分包,在代码里面build.gradle配置文件里面配置
android {
defaultConfig {
minSdkVersion 21
targetSdkVersion 26
multiDexEnabled true //启用分包方案
}
}
然后在编译打包阶段,我们就能看到有classes.dex , classes2.dex 以及更多。
3,热修复的流程。
通过上面一层一层的代码分析,我们可以知道,首先获得应用的PathClassLoader,这个活动很简单,我们只需要调用
context.getClassLoader();
拿到PathClassLoader之后,我们知道编译后的class文件是被打包成Dex文件的,而Dex文件又通过makeDexElements函数转化成Element数组,这个数组dexElements又是DexPathList的一个成员变量 , 而DexPathlist 又位于BaseDexClassLoader里面,也是它的一个成员变量,变量名是pathList ,而且还是一个私有的。所以我们热修复,就是通过反射拿到这个pathList , 然后把我们修复好的补丁包例如叫做patch.dex ,通过makeDexElement函数转化成数组 ,并且与旧的dexElements数组合并 ,最主要的是修复的数组应该放到前面,这样就会先加载到修复好的类。同时,我们应该把程序关掉重新加载,原因在于如下:
假如我们在线更新补丁包,但是之前有bug的类已经被加载过了,那么就只会走findLoadedClass,此时就不会走findClass这个方法, 因此需要重启一下应用。咱以前打王者荣耀,赛季更新完之后经常会关掉程序,打开才会生效,就是这个原因。
4,反射的过程
稍微用代码写写思路(伪代码)
private void ReflectFix(Context context) throws NoSuchFieldException, IllegalAccessException {
ClassLoader classLoader = context.getClassLoader(); //拿到当前应用的ClassLoader ,这种方式拿到的是PathClassLoader,
Class<?> clazz = classLoader.getClass().getSuperclass(); // 因为得到的是PathClassLoader,我们要拿到的是BaseDexClassLoader,这个类是它的父类,因此通过getSuperClass来获得
Field dexElements = clazz.getDeclaredField("pathList"); //拿到dexElements这个数组,变量名是pathList,因为是私有的,通过getDeclaredField获得
dexElements.setAccessible(true); // 禁用安全检查,因为它不是public而是私有的,直接调用会报IllegalAccessException这个异常 ,之后就可以使用get方法拿到对象了。
Object dexPathList = dexElements.get(classLoader);
Object[] objects = makePathElements(dexPathList, optimizedDirectory,suppressedExceptions);
// 这个objects就是我们的Elements数组了,然后用补丁包里面的dex文件和app原本有的classes.dex文件 ,合并生成一个新的数组,赋值即可。
}
后面的流程没有细写,就是通过System.arrayCopy将数组扩容并且进行赋值。
以上。。。