Android热修复原理简介
今天看到塞尔维亚总统在全国电视直播中说到,只有中国才能救我们的时候,作为中国人的那种骄傲油然而生,很幸运能见证中国的崛起和强大,这才是大国当担的样子。
闲话少说,今天准备写一篇关于Android热修复的东西,部分内容来自 享学课堂lance老师
热修复四大框架
首先我们来对看一下主流框架对于热修复的对比图,了解一下各大厂商用的框架对比。热补丁方案有很多,其中比较出名的有腾讯Tinker、阿里的AndFix、美团的Robust以及QZone的超级补丁方案,下面是他们的对比
热修复框架对比图腾讯Tinker:Tinker通过计算对比指定的Base Apk中的dex与修改后的Apk中的dex的区别,补丁包中的内容即为两者差分的描述。运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件。
特点:重启生效、反射、类加载、DexDiff
QQ的Qzone:QQ空间基于的是dex分包方案。把BUG方法修复以后,放到一个单独的dex补丁文件,让程序运行期间加载dex补丁,执行修复后的方法。如何做到这一点?在Android中所有我们运行期间需要的类都是由ClassLoader(类加载器)进行加载。因此让ClassLoader加载全新的类替换掉出现Bug的类即可完成热修复。
特点:重启生效、反射、类加载
美团Robust:对每个函数都在编译打包阶段自动的插入了一段代码。类似于代理,将方法执行的代码重定向到其他方法中。
特点:即时生效、注解、插桩、代理
阿里AndFix:在native动态替换java层的方法,通过native层hook java层的代码。
特点:即时生效、不能替换类,只是通过改变Native层的指针改变所指向的方法,从而完成对方法的修复
以上是各大平台使用热修复方案的优缺点,有些地方可能有些难以理解,这篇文章将着重介绍Qzone的原理和具体实现,其它方案读者可以自行研究,此处只做简单的介绍。
QQ空间Qzone原理
在介绍Qzone的实现原理之前,需要向大家介绍这么几个知识点:
-
类加载机制 classloader的原理
我们知道任何一个类的class对象都会对应一个classloader,表示该类被哪个类加载器加载,Android原生api为我们提供了二种ClassLoader的抽象子类,分别为BootClassLoader,BaseClassLoader
BootClassLoader用于加载Android Framework层的class文件,例于Activity.class等等
BaseDe'xClassLoadexer下面又有两个子类,PathClassLoader,DexClassLoder
PathClassLoader用于加载自己写的类,或者第三方库里面的类,包括android自己开发的第三方库
DexClassLoder 和PathClassLoader一样,都是用来加载class文件
其实两者并没有太大区别,只是构造方法不同而已,谷歌的意思是系统的类用pathclassloader,而我们用户自己写的类用DexClassLoder,但其实两者可以互相替换使用,只不过DexClassLoder比pathclassloader的构造方法多了一个参数,而这个参数只是用来保存我们的odex文件的目录,且在android更高的版本,这个参数也被弃用,被统一保存到系统的目录中。
这些类加载器有一个共同的特性,在加载完一个类的class文件以后,不会再去加载相同的class文件,而我们就是利用这种机制,去实现热修复。
在应用程序启动的时候,所有类的class文件,会被添加到一个Element的数组中,classloader有序的遍历这个数组,当遇见加载过重复的类时,就不会再去加载,所以我们只要想办法,帮我们要修复的class文件添加到这个集合的最前面,也就完成了热修复功能。
-
双亲委托机制
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) {
c = parent.loadClass(name, false);
} else {
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;
}
这是classloader加载类时候的源码,findLoadedClass相当于缓存,如果之前加载过可以直接加载出来。假设我们程序重新启动,代码会执行到 c = parent.loadClass(name, false); 查看源码可知parent为classloader内部维护的一个成员变量classloader parent,这里优先让parent加载类,如果parent没有找到,自己再去找,其实这里面有点类似装饰者模式,我们思考一个问题,在这个内部维护的parent内部是不是也有一个相同的classloader ,然后在查找这个name的时候,又会委托parent内部维护的classloader 去做,直到找不到为止,就自己来找。我们把这种机制称之为双亲委托机制。永远先让父加载器加载。总结入下:
某个类加载器在加载类时,首先将加载任务委托给父 - 类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。
那么为什么会有这个机制呢,
1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。且只有一个classLoader就能加载出来系统所有的class对象
2、安全性考虑,防止核心API库被随意篡改。 (假设我创建一个String类,如果没有这种机制,回导致我们的String类把系统的String替换掉)
掌握以上两点基础知识,我们再来看看classloader是如何去加载一个类的。我们已经了解了,如果我们自己写一个类是会被PathClassLoader加载的,所以parent.loadClass(name, false)是注定找不到我们要修复的类,然后我们看看findClass的逻辑。PathClassLoader没有实现这个方法,我们来看他的父类BaseDexClassLoader的findclass
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
在findclass里面,又是通过pathList来查找,所以我们可以继续查看pathList.finClass做了什么
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
在DexPathList内部,又是通过element来findClass,所以我们最终只要锁定Element这个数组即可。系统会把我们所有dex文件,加载到Element数组中,然后有序遍历,而我们要想给一个类打补丁,就必须要保证这个补丁类的dex文件在错误类dex文件之前加载,而实现步骤就是在这个数组最开始的位置插入这个打了补丁的dex文件即可。(因为数组大小固定,为了避免数组角标越界,我们需要替换这个数组而不是插入)
所以总结一下,想要做到热修复,需要做到如下几步:
-
获取到当前应用的PathClassloader;
-
反射获取到DexPathList属性对象pathList;
-
反射修改pathList的dexElements
3.1 把补丁包patch.dex转化为Element[] (patch)
3.2 获得pathList的dexElements属性(old)
3.3 patch+old合并,并反射赋值给pathList的dexElements
问题:QQ空间兼容问题
https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a
网友评论