参考资料:
安卓App热补丁动态修复技术介绍
Android dex分包方案
在没有热修复之前,我们发布一个app,用户安装到自己的手机上之后,突然发现一个小小的错误,比如一个显示错误,或者一个活动启动逻辑失效,当然这只是举个例子,出现这样的问题后,我们肯定要改代码,打包,重新发布,然后用户重新下载,覆盖安装,先不说我们发布方团队为了这一个小小的错误需要付出的代价,就只看用户这方面,一个小小的错误,让用户重新下载,安装,不考虑流量,重新安装也够用户烦心的。
如果频繁的出现Bug呢?我们频繁的发布版本,让用户频繁的下载,覆盖安装。这样的后果更不用想了,我们会失去一部分用户。
QQ空间团队就针对这个问题提出了一个解决方案–热补丁动态修复。
热修复即在不发布新版本的情况下,用户不需要覆盖安装,我们发布方下发一个补丁包给用户,实现修复。
QQ空间这种修复思想基于dex分包(不清楚的可以看文章开头参考资料),我们一个项目分了很多dex文件,每个dex文件有很多类,一个dex是一个Element,多个dex文件就排列成一个数组dexElements。那么我们是如何加载类的呢?
QQ空间热修复思想的基础依赖:
如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。
我们会按照这个dexElements顺序查找,比如A.class这个类,如果这个类存在两个dex文件里,我们加载的是处于dexElements较前的dex文件里的A.class。QQ空间团队就利用这一点,提出了热修复的解决方案。在这里,插入两张参考资料里的两张图,以便更清楚的理解:
在上面的图中,patch.dex和classes.dex中都有一个Qzone.class,依照上面所说,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。那么我们具体加载的时候就加载的是patch.dex中的Qzone.class。
那么解决方法就有了,就按照上面的图,比如说出问题的类是classes.dex文件里的Qzone.class,我们将这个类修改后重新打包成patch.jar(里面包含dex文件),将这个插入到数组dexElements的最前面,这样,加载Qzone.class时就加载的是我们修改过的patch.dex里的Qzone.class,那个在classes.dex中有问题的Qzone.class就不会加载了。
按照上面的方式处理,会遇到一个问题,校验失败,引用Qzone.class的类和Qzone.class不在一个dex文件会报错,为什么会出这样的问题呢,我们有很多dex文件,其他类引用不同dex文件的类时怎么没有这样的问题呢?
原因在于CLASS_ISPREVERIFIED标记,如果引用Qzone.class的类被打上了这个标记,那就会有校验操作,在校验过程中,发现被校验的类引用了一个与其不在同一个dex文件下的类时,就会报错,我们我们不想让它有校验操作,就要避免引用Qzone.class的类打上这个CLASS_ISPREVERIFIED这个标记。
那么我们就要先知道,什么情况下会打上CLASS_ISPREVERIFIED这个标记。
1. static方法
2. private方法
3. 构造函数
4. 虚函数
如果Qzone.class在以上方法中被引用时,并且Qzone.class和引用Qzone.class的类处于同一个dex文件时,就会打上那个标记。
我们看一下QQ空间团队的方法,他们使用了插桩,向所有类的构造函数中插入了以下代码:
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
并且,将AntilazyLoad.class放入一个单独的dex文件里。
一开始我看这一块的时候,始终没明白为什么插入这个代码后,然后将AntilazyLoad.class放入单独的dex文件下,就不会打上那个标记了。
后来才发现我的理解有问题,我以为问题类是AntilazyLoad.class,后来才发现并不是,这可以是一个没有任何意义的类,它的作用只是想避免打上标记。我们在所有类的构造函数中插入这个代码后,并将AntilazyLoad.class放入一个单独的dex文件后,所有的类都不会打上这个标记,为什么?前面我们说过一个类被打标记的前提:
1.在上面提到的那4个方法中引用了类。
2.该类与引用的类处于同一个dex文件。
再看我们插入代码并将AntilazyLoad.class单独打包之后,我们破坏了上面两个条件的第二个条件,即我们所有的类都引用了一个和所有类都不在一个dex文件中的AntilazyLoad.class这个类。那么所有的类都不会打上CLASS_ISPREVERIFIED这个标记。就不会有校验操作。我们再去引用插入的patch.dex文件下的类时也不会再报错了。
再换一种说法。本来有一些类(并没有引用别的dex文件下的类的类)并没有访问别的dex文件中的类的权限,我们在所有类的构造函数中插入
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
然后又让AntilazyLoad.class处于一个单独的dex文件,那么所有的类都开通了这个权限,可以访问和自己处于不同dex文件下的类。
如果还是不能理解,就再换一种说法。一个类在没有引用其他dex文件下的类时,会打上那个标记,我们为了不让它打上那个标记,就让它引用别的dex文件下的类,哪怕是一个无任何意义的类。这就是QQ空间的解决方法。