1hotfixResearch

价值

热修复的作用

  1. 可快速修复,避免线上Bug带来的业务损失,把损失降到最低。

  2. 保证客户端的更新率,无须用户进行版本升级安装

  3. 良好的用户体验,无感知修复异常。节省用户下载安装成本。

分类

图解

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体积 较大 较小 较小 较小
成功率 较高 较高 一般 最高

代码修复

代码插桩和替换/Java Hook

Instant Run

原理

在第一次完整编译的时候给所有的类插桩(字节码操作),使它们的方法能被代理

代码改动后的增量编译中,通过gradle插件生成包含了改动代码的代理类

通过app中的instant-run服务给代码被改动的类的$change字段复制,这样所有方法都转发到了代理类,而代理类里就是改动后的逻辑

Robust

美团, 开源,实时修复

原理

1、打基础包时插桩,在每个方法前插入一段类型为 ChangeQuickRedirect 静态变量的逻辑,插入过程对业务开发是完全透明

2、加载补丁时,从补丁包中读取要替换的类及具体替换的方法实现,新建ClassLoader加载补丁dex。当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的

优缺点

优点

· 高兼容性(Robust只是在正常的使用DexClassLoader)、高稳定性,修复成功率高达99.9%

· 补丁实时生效,不需要重新启动

· 支持方法级别的修复,包括静态方法

· 支持增加方法和类

· 支持ProGuard的混淆、内联、优化等操作

缺点

· 代码是侵入式的,会在原有的类中加入相关代码

· so和资源的替换暂时不支持

· 会增大apk的体积,平均一个函数会比原来增加17.47个字节,10万个函数会增加1.67M

NativeHook

AndFix

开源,实时生效

HotFix

阿里百川,未开源,免费、实时生效

HotFix是AndFix的优化版本,Sophix是HotFix的优化版本。目前阿里系主推是Sophix。

原理

每一个Java方法在art中都对应一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括访问权限及代码执行地址等。通过env->FromReflectedMethod得到方法对应的ArtMethod的真正开始地址,然后强转为ArtMethod指针,从而对其所有成员进行修改。

这样以后调用这个方法时就会直接走到新方法的实现中,达到热修复的效果。

优缺点

优点

· 即时生效

· 没有性能开销,不需要任何编辑器的插桩或代码改写

缺点

· 存在稳定及兼容性问题。ArtMethod的结构基本参考Google开源的代码,各大厂商的ROM都可能有所改动,可能导致结构不一致,修复失败。

· 无法增加变量及类,只能修复方法级别的Bug,无法做到新功能的发布

Java multiDex

Qzone

超级补丁 QQ空间,未开源,冷启动修复

QFix

手Q团队,开源,冷启动修复

Nuwa

大众点评,开源,冷启动修复

原理

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中让其他类调用。

Java Dex 替换

Tinker

微信团队,开源,冷启动修复。提供分发管理,基础版免费

Amigo

饿了么,开源,冷启动修复

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,最后导致合并失败

混合优化

Sophix

未开源,商业收费,实时生效/冷启动修复

资源修复

全量包

Amigo

饿了么

Instant Run

原理

资源修复原理 Instant Run(替换AssertManager)

1、构建一个新的AssetManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到AssetManager中。这样就得到一个含有所有新资源的AssetManager

2、找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager

具体实现查看insatnt-run.jar中的MonkeyPatcher#monkeyPatchExistingResources()

差量包

Tinker

Sophix

原理

Sophix

我们发现,其实大量代码都是在处理兼容性问题和找到所有AssetManager的引用处,真正的替换的逻辑其实很简单。

我们的方案没有直接使用Instant Run的技术,而是另辟蹊径,构造了一个package id为0x66的资源包,这个包里只包含改变了的资源项,然后直接在原有AssetManager中addAssetPath这个包就可以了。

由于补丁包的package id为0x66,不与目前已经加载的0x7f冲突,因此直接加入到已有的AssetManager中就可以直接使用了。补丁包里面的资源,只包含原有包里面没有而新的包里面有的新增资源,以及原有内容发生了改变的资源。并且,我们采用了更加优雅的替换方式,直接在原有的AssetManager对象上进行析构和重构(直接在原有AssetManager中addAssetPath这个包,加到已有的AssetManager中),这样所有原先对AssetManager对象的引用是没有发生改变的,所以就不需要像Instant Run那样进行繁琐的修改了。

可以说,我们的资源修复方案,优越性超过了Google官方的Instant Run方案。整个资源替换的方案优势在于:

  1. 不修改AssetManager的引用处,替换更快更完全。(对比Instanat Run以及所有copycat的实现)

  2. 不必下发完整包,补丁包中只包含有变动的资源。(对比Instanat Run、Amigo等方式的实现)

  3. 不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比Tinker的实现)

SO修复

接口调用转换

Tinker

原理

sdk提供接口替换System默认加载so库的接口

SOPatchManger.loadLibrary( StringlibName)

替换

System.loadLibrary( StringlibName)

SOPatchManger.loadLibrary接口加载so库的时候优先尝试去加载sdk指定目录下补丁的so。若不存在,则再去加载安装apk目录下的so库

优缺点

优点:不需要对不同sdk版本进行兼容,所以sdk版本都是System.loadLibrary这个接口

缺点:需要侵入业务代码,替换掉System默认加载so库的接口

插桩实现

Amigo 饿了么

Sophix

原理

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热修复技术原理详解

Android热修复技术原理(最新最全)

Android热修复技术,你会怎么选?

《深入探索Android热修复技术原理7.3Q.pdf》