[原文链接](https://m.study.163.com/article/1048047024?utm_campaign=share&utm_medium=androidShare&utm_u=1017421009&utm_source=qq)
前言
今年真是热补丁框架的洪荒之力爆发的一年,短短几个月内,已经出现了好几个热修复的框架了,基本上都是大同小异,这里我就不过多的去评论这些框架。只有自己真正的去经历过,你才会发现其中的
事实上,现在出现的大多数热修复的框架,稳定性和兼容性都还达不到要求,包括阿里的Andfix,据同事说,我们自己的app原本没有多少crash,接入了andfix倒引起了一部分的crash,这不是一个热修复框架所应该具有的“变态功能”。虽然阿里百川现在在大力推广这套框架,我依旧不看好,只是其思路还是有学习价值的。
**Dex的热修复总结**
Dex的热修复目前来看基本上有四种方案:
* 阿里系的从native层入手,见AndFix
* QQ空间的方案,插桩,见安卓App热补丁动态修复技术介绍
* 微信的方案,见微信Android热补丁实践演进之路,dexDiff和dexPatch,方法很牛逼,需要全量插入,**但是这个全量插入的dex中需要删除一些过早加载的类**,不然同样会报class is pre verified异常,还有一个缺点就是合成占内存和内置存储空间。微信读书的方式和微信类似,见Android Patch 方案与持续交付,不过微信读书是miniloader方式,启动时容易ANR,在我锤子手机上变现出来特别明显,长时间的卡图标现象。
* 美团的方案,也就是instant run的方案,见Android热更新方案Robust
此外,微信的方案是多classloader,这种方式可以解决用multidex方式在部分机型上不生效patch的问题,同时还带来一个好处,这种多classloader的方式使用的是instant run的代码,如果存在native library的修复,也会带来极大的方便。
**Native Library热修复总结**
而native libraray的修复,目前来说,基本上有两种方案。
* 类似multidex的dex方式,插入目录到数组最前面,具体文章见Android热更新之so库的热更新,需要处理系统的兼容性问题,系统分隔线是Android 6.0
* 第二种方式需要依赖多classloader,在构造BaseDexClassLoader的时候,获取原classloader的native library,通过环境变量分隔符(冒号),将patch的native library与原目录进行连接,patch目录在前,这样同样可以达到修复的目的,缺点是需要依赖dex的热修复,优点是应用native library时不需要处理兼容性问题,当然从patch中释放出来的时候也需要处理兼容性问题。
第二种方式的实现可以看看BaseDexClassLoader的构造函数
~~~
BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent)
~~~
只需要在修复dex的同时,如果有native library,则获取原来的路径与patch的路径进行连接,伪代码如下:
![](https://box.kancloud.cn/47c5cd89417bd84b2a21e0a408c49f17_640x139.png)
而这种方式需要强依赖dex的修复,如果没有dex,就无能为例了,实际情况基本上是两种方式交叉使用,在没有dex的情况下,使用另外一种方式。
而native library还有一个坑,就是从patch中释放so的过程,这个过程需要处理兼容性,在Android 21以下,通过下面这个函数去释放
![](https://box.kancloud.cn/14e235b60e7a8f18f06de5bf7a1e5be8_640x29.png)
而在andrdod 21及以上,则通过下面的这几个函数去释放
![](https://box.kancloud.cn/84aadb7f24830061379e6e2cb3c6fa15_640x64.png)
**资源的热修复**
而对于资源的热修复,其实主要还是和插件化的思路是一样的,具体实现可以参考两个
* Atlas或者携程的插件化框架
* Instant run的资源处理方式,甚至可以做到运行期立即生效。
本篇文章就来说说资源的热修复的实现思路,在这之前,需要贴两个链接,以下文章的内容基于这两个链接去实现,所以务必先看看,不然会一脸懵逼。一个是instant run的源码,自备梯子,另一个是冯老师写的一个类,这个类在Atlas中出现过,后来被冯老师重写了,同样自备梯子。
* instant-run源码
* Hack.java实现
资源的热修复实现,主要由一下几个步骤组成:
* 提前感知系统兼容性,不兼容则不进行后续操作
* 服务器端生成patch的资源,客户端应用patch的资源
* 替换系统AssetManger,加入patch的资源
对于第一步,我们需要先看看instant run对于资源部分的实现,其伪代码如下
![](https://box.kancloud.cn/c36cf14d41c05d5918a4fb8e5b63a40f_640x370.png)
代码很简单,通过调用addAssetPath将patch的资源加到新建的AssetManager对象中,然后将内存中所有Resources对象中的AssetManager对象替换为新建的AssetManager对象。当然还需要处理兼容性问题,对于兼容性问题,则需要用到冯老师的Hack类(这里我为了与原来冯老师没有重写前的Hack类做区分,将其重命名了HackPlus,意思你懂的),具体Hack过程请参考Atlas或者携程的插件化框架的实现,然后基于instant run进行实现,当然这种方式有一部分资源是修复不了了,比如notification。
* 主要的分界线是Android 19 和 Android N
* 首先需要拿到App运行后内存中的Resources对象
* Android N,通过ResourcesManager中的mResourceReferences去获取Resources对象,是个ArrayList对象
* Android 19到Android N(不含N),通过ResourcesManager中的mActiveResources去获取Resources对象,是个ArrayMap对象
* Android 19以下,通过ActivityThread的mActiveResources去获取Resources对象,是个HashMap对象。
* 接着就是替换Resources中的AssetManager对象
* Android N,替换的是Resources对象中的mResourcesImpl成员变量中的mAssets成员变量。
* Android N以前,替换的是Resources对象中的mAssets成员变量。
* 对于Android 19以下,ActivityThread是通过ActivityThread中的静态函数currentActivityThread获取的的,这里有个坑,如果在主线程获取还好,但是万一在子线程获取,在低版本的Android上可能就是Null,因为在低版本,这个变量是通过ThreadLocal进行存储的,对于这种情况,只要检测当前线程是不是主线程,如果是主线程,则直接获取,如果不是主线程,则阻塞当前线程,然后切换到主线程获取,获取完成后通知阻塞线程。
这里我已经基本实现了反射检测系统支持性相关的代码,主要就是对以上分析的内容做反射检测,一旦发生异常,则不再进行资源的修复,代码如下(HackPlus的源码见上面的Hack.Java的源码):
![](https://box.kancloud.cn/1b552e39ab6a225dc7b038cd9226d9fe_640x602.png)
![](https://box.kancloud.cn/bda26eb378e26af6a5c2505d5b2e38ab_640x541.png)
![](https://box.kancloud.cn/57152b99d892bf71ed002c6f74cd32d7_640x633.png)
![](https://box.kancloud.cn/a75d46e58445fe5cba30f15e0ba50fec_640x635.png)
![](https://box.kancloud.cn/95fee296efbecde3ba6c094898d68bbd_640x649.png)
![](https://box.kancloud.cn/d35bcf20607aa69dd4f644b9ebc0f535_640x610.png)
![](https://box.kancloud.cn/4172cf0669e3d39eafb61fb85497d147_640x347.png)
使用的时候,只要在加载patch资源前,调用如下方法进行检测
![](https://box.kancloud.cn/b4871be689cc09235bb7b43d01fc1301_640x103.png)
patch资源的生成比较麻烦,我们放在最后面说明,现在假设我们有一个包含整个apk的资源的文件,需要运行时替换,现在来实现上面的加载patch资源的逻辑,具体逻辑上面反射的时候已经说明了,这时候只需要调用上面反射获取的包装类,进行替换即可,直接看代码中的注释:
![](https://box.kancloud.cn/aed6154667d36446e537b6c23ca34ecd_640x580.png)
![](https://box.kancloud.cn/47228cad532a37b5fe538d57aadf7637_640x634.png)
![](https://box.kancloud.cn/0dd8161bddd21d8ad6f97ea02b97d53a_640x61.png)
这样一来,就在Appliction启动的时候完成了资源的热修复,当然我们也可以像instant run那样,把activity也处理,不过我们简单起见,让其重启生效,所以activity就不处理了。
于是,我们Appliction的onCreate()中的代码就变成了下面这个样子
![](https://box.kancloud.cn/063c71fcc76beb950ff3da8bea21d03c_640x175.png)
这里有一个坑。
> patch应用成功后,如果要删除patch,patch文件的删除一定要谨慎,最好先通过配置文件标记patch不可用,下次启动时检测该标记,然后再删除,运行期删除正在使用的patch文件会导致所有进程的重启,Application中的所有逻辑会被初始化一次。
还差最后一步,patch的资源从哪里来,这里主要讲两种方式。
* 直接下发整个apk文件,全量的资源,想怎么用就怎么用,当然缺点很明显,文件太大了,下载容易出错,不过也最简单。
* 下发patch部分的资源,在客户端和没改变的资源合成新的apk,这种方式的优点是文件小,缺点是合成时占内存,需要开启多进程去合成,比较复杂,没有办法校验合成文件的md5值。
**无论哪一种方式,都需要public.xml去固定资源id。**
这里讨论的是第二种方式,所以给出精简版的实现思路:
首先需要生成public.xml,public.xml的生成通过aapt编译时添加-P参数生成。相关代码通过gradle插件去hook Task无缝加入该参数,有一点需要注意,通过appt生成的public.xml并不是可以直接用的,该文件中存在id类型的资源,生成patch时应用进去编译的时候会报resource is not defined,解决方法是将id类型的资源单独记录到ids.xml文件中,相当于一个声明过程,编译的时候和public.xml一样,将ids.xml也参与编译即可。
![](https://box.kancloud.cn/f19b46bc112a474bd09f8296d49de3e7_640x623.png)
![](https://box.kancloud.cn/c2b734dee6548216c23b99a08b1a7f8f_640x484.png)
在编译资源之前,将public.xml和ids.xml文件拷贝到资源目录values下,并检测values.xml文件中是否有已经定义的id类型的资源,如果有,则从ids.xml文件中将其删除,否则会报resource is already defined的异常,也会编译不过去。
![](https://box.kancloud.cn/88e77e5e8ca62a53568c6e1a1a06e140_640x656.png)
![](https://box.kancloud.cn/4b550aa43d597847c71e088a7c27ed68_640x629.png)
![](https://box.kancloud.cn/8ffefb8df3bc1f1cceccc337b431f926_640x138.png)
这样一来,按照正常流程去编译,生成的apk安装包就可以获得了,然后将这个new.apk和有问题的old.apk进行差量算法,这里只考虑资源相关文件,即assets目录,res目录,arsc文件,AndroidManifest.xml文件,相关算法如下:
* 对比new.apk和old.apk中的所有资源相关的文件。
* 对于新增资源文件,则直接压入patch.apk中。
* 对于删除的资源文件,则不处理到patch.apk中。
* 对于改变的资源文件,如果是assets或者res目录中的资源,则直接压缩到patch.apk中,如果是arsc文件,则使用bsdiff算法计算其差量文件,压入patch.apk,文件名不变。
* 对于改变和新增的文件,通过一个meta文件去记录其原始文件的adler32和合成后预期文件的adler32值,以及文件名,这是个文本文件,直接压缩到patch.apk中去。
* 对patch.apk进行签名。
这样做的好处是能将资源patch文件尽可能的减小到最低,实际情况严重下来,res目录下的资源文件大小都非常小,没有必要去进行diff,所以直接使用原文件,而arsc文件则相对比较大,在考虑文件大小和内存的两个因素下,牺牲内存换大小还是ok的,所以在下发前,我们对其进行diff,生成diff文件,在客户端进行合成最终的arsc文件。
客户端下载到patch.apk后需要进行还原,还原的步骤如下:
* 考虑到客户端jni的兼容性问题,bspatch算法全部使用java实现
* 首先校验patch.apk的签名
* 读取压缩包中meta文件,判断哪些文件是新增文件,哪些文件是改变的文件。
* 遍历patch.apk中的文件,如果是新增文件,则压缩到new.apk文件中去
* 如果是改变的文件,如果是assets和res文件夹下的资源,则直接压缩到new.apk文件中,如果是arsc文件,则应用bspatch算法合成最终的arsc文件,压缩到new.apk中
* 如果文件没有改变,则直接复制old.apk中的原始文件到new.apk中
* 以上任何一个步骤都会去校验合成时旧文件的adler32和合成后的adler32值和meta文件中记录的是否符合
* 由于无法验证合成后的文件的md5值(没有记录哪些文件被删除了,加上压缩算法等原因),需要使用一种方式在加载前进行验证,这里使用crc32值。
* 合成成功后计算new.apk文件的crc32值,计算方式进行改进,不计算所有文件内容的crc32,为了快速计算,只计算文件的某一个特定段的crc32值,比如文件从200字节开始到2000字节部分的crc32值,并保存在sharePrefrences中,加载patch前进行校验crc32,校验不通过,则直接删除patch文件,当然这种计算方式有一定概率会把错误的文件当成正确的,毕竟计算的不是完整的文件,当然正确的文件是一定不会当成错误的,这种低概率事件可以接受。
这种方式的兼容性如何?简单自测了下,4.0-7.0的模拟器运行全部通过,当然不排除国产奇葩ROM的兼容性,所以这里我不宣称100%兼容。
无图言屌,没图你说个jb,先上一张没有进行热修复的图:
![](https://box.kancloud.cn/5012d571a0ff1391adb12542fc5b11f2_640x1138.png)
热修复之后的效果图
![](https://box.kancloud.cn/a94fbdf25273bc4c7ffe1b440cea8445_640x1138.png)
最后送上一句话:
热修复远远没有你想象的那么简单,踩坑之路漫漫,入坑需谨慎。
- 前言
- Android 热补丁技术——资源的热修复
- 插件化系列详解
- Dex分包——MultiDex
- Google官网——配置方法数超过 64K 的应用
- IMOOC热修复与插件化笔记
- 第1章 class文件与dex文件解析
- Class文件解析
- dex文件解析
- class与dex对比
- 第2章 虚拟机深入讲解
- 第3章 ClassLoader原理讲解
- 类的加载过程
- ClassLoade源码分析
- Android中的动态加载
- 第4章 热修复简单讲解
- 第5章 热修复AndFix详解
- 第6章 热修复Tinker详解及两种方式接入
- 第7章 引入热修复后代码及版本管理
- 第8章 插件化原理深入讲解
- 第9章 使用Small完成插件化
- 第10章 使用Atlas完成插件化
- 第11章 课程整体总结
- DN学院热修复插件化笔录
- 插件化
- 热修复
- Android APP开发应掌握的底层知识
- 概述
- Binder
- AIDL
- AMS
- Activity的启动和通信原理
- App启动流程第2篇
- App内部的页面跳转
- Context家族史
- Service
- BroadcastReceiver
- ContentProvider
- PMS及App安装过程