热修复,即热加载。是指在一个App正常打开之后,从外部加载本不属于它的一些资源并加以运用的功能。以下demo只是基于热修复的原理做到了初步实现,另一个关键点 dex插桩 还有待下一篇文章详细讲解,因此这一版的demo并不能投入实际的商业化运用
热修复的实现的途径有很多种,其中一种是通过反射来拿到当前应用程序的 ClassLoader 中的成员变量:pathList 所指向的实例。然后再通过反射这个实例,拿到其内部成员变量:dexElements数组 的值。由于安卓系统在加载一个类之前会从前往后地去遍历这个 dexElements数组,寻找当前需要加载的 .class 文件。因此,只要我们将外部下载的 apk包 或者是 .dex 文件路径生成的对象插入到 dexElements数组 的最前方,就可以达到顶替数组后面原有的同名 .class 文件的目的,从而实现外部热修复。示意图如下(patch.dex就是外部加载进来的修复包):
因此,在整个热加载功能实现过程中(包括插件化和so库动态加载):Classloader -> 内部DexPathList类型的成员变量 -> DexPathList中的成员变量Elements数组的更改,是整个功能中比较核心的一环,运用类加载的实现方式完成热修复的框架,源码中一定有反射并修改elements数组的相关逻辑。
根据以上提到的思路,我们来看一下核心的相关代码:
拿到 ClassLoader 和 修复包的存储路径 之后,就可以开始准备做 java反射了,这一块的逻辑我封装到了 installSecondaryDexes 方法中
根据不同的系统SDK版本执行不同的反射逻辑(我们以 api19 及其以上的代码为例)
接下来获取到 dexElements 数组,准备在数组第一位插入外部载入的修复包路径
由上图可见,截图中的最后一个方法 makeDexElements 就是插入外部修复包路径的封装方法,这个方法的第三个实参又是一个方法,这个方法是根据不同的手机SDK版本,将修复包的路径封装成不同类型的对象组成的数组,这个数组最后会被插入到 dexElements 数组当中。经过了 expandFieldArray 这个方法的执行之后,原先存储 外部修复包 的路径下就多出了一个 .dex 文件,或者 .odex 和 .vdex 文件。makeDexElements 方法的代码如下:
我们再回到外层的 expandFieldArray 方法,其内部的逻辑是这样的:首先通过反射拿到dexElements的取值,然后将上图方法获取到的 object[] 插入到数组的最前面。这个被插入的 object[] 数组就是外部修复包存储路径集合编译后形成的队列,也就是外部修复包的资源和 .class 队列
以上步骤完成之后,当前App的 dexElements 的状态就变成了这样,理论上是可以顶替原包的同名 .class 文件了:
接下来我们做一些外部的封装,比如说断点续传下载外部修复包,以及在Splash页面上做热修复准备处理等等,original包和fix包的代码都在下面的git链接上了:
https://github.com/liuchenguangqnm/hot_fix_example
然后我们再在俩包的MainActivity上分别写上不同的吐司显示准备测试:
然后找到 original_package 的 Appconfig 类,配置好修复包的下载地址,以及下载到本地的存储文件名
最后就可以安装看效果了:
首先第一次打开,因为修复包需要下载时间,所以第一次打开App或者断网的时候修复包是没有插入完成的,所以此时点击按钮提示如下:
等到第一次的修复包插入完成之后,关闭MainActivity,再次打开,点击测试按钮,吐司显示如下:
以上代码经本人测试,可以顺利在安卓各个模拟器和小米4上面跑通。由于开篇的时候我就说过了,此版本demo由于没有做 dex插桩,因此不能投入商业化使用,所以,以上的demo在华为手机上是跑不通的。
那么什么是dex插桩呢?
我们首先围绕上面讲到的问题出发:为什么有的手机按照demo上的代码运行可以正常走通,有些就走不通,难道有的手机加载类的时候不是通过遍历 dexElements 数组获得 .class 文件的吗?当然这是不可能的。以上demo之所以走不通,是因为新版本的手机SDK为了提高App的启动速度,已经在我们安装应用的时候预先加载了安装包里的所有 .class 文件的索引,有了这个索引,我们再打开App的时候,系统就再不用去 遍历DexElements数组 寻找对应的 .class 文件了。
因为每次加载这个类,新安卓版本的系统都不用再去遍历 DexElements数组 了,因此我们在 App启动之后,再去对 DexElements数组 的内容进行操作,其实都是无意义的,因为系统根本不会再次遍历它了。而解决这个问题的途径之一就是 dex插桩!
首先,我们要清楚的是,系统在安装了一个新的App之后,首先会查看这个 App 里面有多少 .dex 文件。如果一个 .class 文件里面使用到的所有类,都在一个 .dex 文件之中存放的话,那么系统就会给这个类打上一个 CLASS_ISPREVERIFIED 标记,有了这个标记,下次App如果要再加载这个类的时候,就不会再次遍历 DexElements 数组了;如果一个 .class 文件里面使用到的所有类,分别在两个 .dex 文件之中的话,那么为了避免在类运行过程中,因为其中一个 .dex 文件的缺失而导致异常,此时这个类不会被打上 CLASS_ISPREVERIFIED 标记,每次我们要加载它的时候,系统还是要遍历一次 dexElements 数组,确保所有的 .dex 文件都是全乎的。
因此,我们需要实现的就是,在安卓打包的时候,给所有有可能出bug的类的构造方法里面都插入一行初始化其它 .dex 文件中的类型的代码,只要有了这行代码,这个类在每次加载之前就必须要重新过一遍 dexElements 数组,我们的修复包就可以趁着这个时机,顶掉存在bug的类。这种在打包时修改 .class 文件内容的技术,就叫做 dex插桩
由于时间和精力有限,再加上dex插桩技术本人也需要花更多的时间去测试和研究,因此我会在下一篇博客继续完善上面分享过的demo
以上,本篇完结
网友评论