可快速修复,避免线上Bug带来的业务损失,把损失降到最低。
保证客户端的更新率,无须用户进行版本升级安装
良好的用户体验,无感知修复异常。节省用户下载安装成本。
graph LR
subgraph dex插入到数组的最前
DexElements
end
subgraph env->FromReflectedMethod得到方法对应的ArtMethod,对其所有成员进行修改
NativeHook
end
subgraph 避免类被加上ISPREVERIFIED
补丁dex
end
代码修复-->NativeHook
代码修复-->DexElements
DexElements-->补丁dex
DexElements-->全量dex
代码修复-->混合优化
代码修复-->代码插桩和替换
https://github.com/Tencent/tinker/wiki
Tinker | QZone | AndFix | Robust | |
---|---|---|---|---|
类替换 | yes | yes | no | no |
So替换 | yes | no | no | no |
资源替换 | yes | yes | no | no |
全平台支持 | yes | yes | yes | yes |
即时生效 | no | no | yes | yes |
性能损耗 | 较小 | 较大 | 较小 | 较小 |
补丁包大小 | 较小 | 较大 | 一般 | 一般 |
开发透明 | yes | yes | no | no |
复杂度 | 较低 | 较低 | 复杂 | 复杂 |
gradle支持 | yes | no | no | no |
Rom体积 | 较大 | 较小 | 较小 | 较小 |
成功率 | 较高 | 较高 | 一般 | 最高 |
在第一次完整编译的时候给所有的类插桩(字节码操作),使它们的方法能被代理
代码改动后的增量编译中,通过gradle插件生成包含了改动代码的代理类
通过app中的instant-run服务给代码被改动的类的$change字段复制,这样所有方法都转发到了代理类,而代理类里就是改动后的逻辑
美团, 开源,实时修复
1、打基础包时插桩,在每个方法前插入一段类型为 ChangeQuickRedirect 静态变量的逻辑,插入过程对业务开发是完全透明
2、加载补丁时,从补丁包中读取要替换的类及具体替换的方法实现,新建ClassLoader加载补丁dex。当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的
优点
· 高兼容性(Robust只是在正常的使用DexClassLoader)、高稳定性,修复成功率高达99.9%
· 补丁实时生效,不需要重新启动
· 支持方法级别的修复,包括静态方法
· 支持增加方法和类
· 支持ProGuard的混淆、内联、优化等操作
缺点
· 代码是侵入式的,会在原有的类中加入相关代码
· so和资源的替换暂时不支持
· 会增大apk的体积,平均一个函数会比原来增加17.47个字节,10万个函数会增加1.67M
开源,实时生效
阿里百川,未开源,免费、实时生效
HotFix是AndFix的优化版本,Sophix是HotFix的优化版本。目前阿里系主推是Sophix。
每一个Java方法在art中都对应一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括访问权限及代码执行地址等。通过env->FromReflectedMethod得到方法对应的ArtMethod的真正开始地址,然后强转为ArtMethod指针,从而对其所有成员进行修改。
这样以后调用这个方法时就会直接走到新方法的实现中,达到热修复的效果。
优点
· 即时生效
· 没有性能开销,不需要任何编辑器的插桩或代码改写
缺点
· 存在稳定及兼容性问题。ArtMethod的结构基本参考Google开源的代码,各大厂商的ROM都可能有所改动,可能导致结构不一致,修复失败。
· 无法增加变量及类,只能修复方法级别的Bug,无法做到新功能的发布
超级补丁 QQ空间,未开源,冷启动修复
手Q团队,开源,冷启动修复
大众点评,开源,冷启动修复
Android内部使用的是BaseDexClassLoader、PathClassLoader、DexClassLoader三个类加载器实现从DEX文件中读取类数据,其中PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader实现。dex文件转换成dexFile对象,存入Element[]数组,findclass顺序遍历Element数组获取DexFile,然后执行DexFile的findclass。
所以此方案的原理是Hook了ClassLoader.pathList.dexElements[],将补丁的dex插入到数组的最前端。因为ClassLoader的findClass是通过遍历dexElements[]中的dex来寻找类的。所以会优先查找到修复的类。从而达到修复的效果
优点
· 不需要考虑对dalvik虚拟机和art虚拟机做适配
· 代码是非侵入式的,对apk体积影响不大
缺点
· 需要下次启动才修复
· 性能损耗大,为了避免类被加上CLASS_ISPREVERIFIED,使用插桩,单独放一个帮助类在独立的dex中让其他类调用。
微信团队,开源,冷启动修复。提供分发管理,基础版免费
饿了么,开源,冷启动修复
Amigo 原理与 QQZone 的方案有些类似,QQZone,Tinker,Nuwa这类方案是通过修改PathClassLoader中的dex实现的,Amigo则是釜底抽薪直接替换ClassLoader。同时进一步实现了 so 文件、资源文件、四大组件的修复,可以对APP全面进行修复,不愧 Amigo(朋友)这个称号,能在危急时刻送来全面的帮助。同时Amigo也致力解决使用过程中的带来的束缚,比如对代码进行插桩、打包时保存和指定映射文件等(如果mapping文件丢了后果不堪设想),所以开发和打包完全无侵入。
为了避免dex插桩带来的性能损耗,dex替换采取另外的方式。原理是提供dex差量包,整体替换dex的方案。差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并成一个完整的dex,完整dex加载得到dexFile对象作为参数构建一个Element对象然后整体替换掉旧的dex-Elements数组。
Tinker自研了DexDiff/DexMerge算法。Tinker还支持资源和So包的更新,So补丁包使用BsDiff来生成,资源补丁包直接使用文件md5对比来生成,针对资源比较大的(默认大于100KB属于大文件)会使用BsDiff来对文件生成差量补丁。
优点
· 兼容性高
· 补丁小
· 开发透明,代码非侵入式
缺点
· 冷启动修复,下次启动修复
· Dex合并内存消耗在vm head上,容易OOM,最后导致合并失败
未开源,商业收费,实时生效/冷启动修复
饿了么
资源修复原理 Instant Run(替换AssertManager)
1、构建一个新的AssetManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到AssetManager中。这样就得到一个含有所有新资源的AssetManager
2、找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager
具体实现查看insatnt-run.jar中的MonkeyPatcher#monkeyPatchExistingResources()
我们发现,其实大量代码都是在处理兼容性问题和找到所有AssetManager的引用处,真正的替换的逻辑其实很简单。
我们的方案没有直接使用Instant Run的技术,而是另辟蹊径,构造了一个package id为0x66的资源包,这个包里只包含改变了的资源项,然后直接在原有AssetManager中addAssetPath这个包就可以了。
由于补丁包的package id为0x66,不与目前已经加载的0x7f冲突,因此直接加入到已有的AssetManager中就可以直接使用了。补丁包里面的资源,只包含原有包里面没有而新的包里面有的新增资源,以及原有内容发生了改变的资源。并且,我们采用了更加优雅的替换方式,直接在原有的AssetManager对象上进行析构和重构(直接在原有AssetManager中addAssetPath这个包,加到已有的AssetManager中),这样所有原先对AssetManager对象的引用是没有发生改变的,所以就不需要像Instant Run那样进行繁琐的修改了。
可以说,我们的资源修复方案,优越性超过了Google官方的Instant Run方案。整个资源替换的方案优势在于:
不修改AssetManager的引用处,替换更快更完全。(对比Instanat Run以及所有copycat的实现)
不必下发完整包,补丁包中只包含有变动的资源。(对比Instanat Run、Amigo等方式的实现)
不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比Tinker的实现)
sdk提供接口替换System默认加载so库的接口
SOPatchManger.loadLibrary( StringlibName)
替换
System.loadLibrary( StringlibName)
SOPatchManger.loadLibrary接口加载so库的时候优先尝试去加载sdk指定目录下补丁的so。若不存在,则再去加载安装apk目录下的so库
优点:不需要对不同sdk版本进行兼容,所以sdk版本都是System.loadLibrary这个接口
缺点:需要侵入业务代码,替换掉System默认加载so库的接口
so库的修复本质上是对native方法的修复和替换。
采用类似类修复的反射注入方式。把补丁so库的路径加到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库,而不是原来的so库,从而达到修复的目的。
优点:不需侵入用户接口调用
缺点:需要做版本兼容控制,兼容性较差
使用热修复技术后由于发布流程的变化,肯定也需求采用相应的分支管理进行控制。
通常移动开发的分支管理采用特性分支,如下:
分支 | 描述 |
---|---|
master | 主分支(只能merge,不能commit,设置权限),用于管理线上版本,及时设置对应Tag |
dev | 开发分支,每个新版本的研发根据版本号基于主分支创建,测试通过验证后,上线合入master分支 |
function X | 功能分支,按需求设定。基于开发分支创建,完成功能开发后合入dev开发分支 |
接入热修复后,推荐可参考如下分支策略:
分支 | 描述 |
---|---|
master | 主分支(只能merge,不能commit,设置权限),用于管理线上版本,及时设置对应Tag(一般3位版本号) |
hot_fix | 热修复分支。基于master分支创建,修复紧急问题后,测试推送后,将hot_fix再合并到master分支。再次为master分支打tag。(一般4位版本号) |
dev | 开发分支,每个新版本的研发根据版本号基于主分支创建,测试通过验证后,上线合入master分支 |
function X | 功能分支,按需求设定。基于开发分支创建,完成功能开发后合入dev开发分支 |
注意热修复分支的测试及发布流程应用正常版本流程一致,保证质量。
目前主流的热修复方案,像Tinker及Sophix都会提供补丁的分发及监控。这也是我们选择热修复技术方案需要考虑的关键因素之一。毕竟为了保证线上版本的质量,分发控制及实时监测必不可少。
《深入探索Android热修复技术原理7.3Q.pdf》