目前Android业内,热修复技术百花齐放,各大厂都推出了自己的热修复方案,使用的技术方案也各有所异,当然各个方案也都存在各自的局限性。在面对众多的方案,希望通过梳理这些热修复方案的对比及实现原理,掌握热修复技术的本质,同时也对项目接入做好准备。
简单来说,就是通过下发补丁包,让已安装的客户端动态更新,让用户可以不用重新安装APP,就能够修复软件缺陷的一种技术。
随着热修复技术的发展,不仅可以修复代码,同时可以修复资源文件及SO库。
目前最快捷的集成方式是 集成 bugly 升级sdk。
需要的工作
代理 Application
接入 tinker-support 插件
编写tinker gradle 脚本
每次打包需要修改tinker gradle 脚本的配置
测试下来发现激活成功率 大概 60%
主流配置的机器,平均合成时间 1800秒
基础版 免费:最大补丁大小:500k 日请求量 <1万
专业版 399 - 2899元/月
补丁即时生效,不需要应用重启;
补丁包同样采用差量技术,生成的PATCH体积小;
对应用无侵入,几乎无性能损耗;
两行代码,傻瓜式接入。
免费阈值:月活设备(MAU): 5万。
每个月,每台设备收费0.015元。
计费周期:系统每日生成账单,进行结算。当月收取过的,不再进行收费。即只计算日增量设备
java hook 插桩
无差别兼容Android2.3-8.0版本;
无需重启补丁实时生效,
补丁修补成功率高达99.9%(所有热修复方案中成功率最高的)
只支持方法级别的Bug修复,不支持资源及so
bug修复通过java hook 代码实现
补丁的下发和合并等需要自己实现
如果考虑付费,推荐选择阿里的Sophix,Sophix是综合优化的产物,功能完善、开发简单透明、提供分发及监控管理。
如果不考虑付费,只需支持方法级别的Bug修复,不支持资源及so,推荐使用Robust。补丁修补成功率高达99.9%(所有热修复方案中成功率最高的)
如果考虑需要同时支持资源及so,使用Tinker。不建议使用,因为实现原理是 dex合成后替换,dex合成成功率不高(60%)
如果公司综合实力强,可考虑自研,灵活性及可控制最强。
从Github上的热度及提交记录上看,nuwa、AndFix、Amigo等的提交都是2 years ago。
底层替换和类加载(dex插入/替换)
类加载有两种实现:dexElements和替换dex;所以又称三大流派
美团 Robust 这种 java方法插桩hook的,只能实现代码修复,无法实现资源和so修复;所以不在常规讨论范围内。
代表:阿里系的 Andfix HotFix
通过Andfix提供的工具对比出新旧apk 的 classes.dex 文件的差异,并生成patch压缩包(jar包)
压缩包中比较关键的是 PATCH.MF (补丁类名)和 diff.dex (补丁方法)
虚拟机通过 jar包 读取 补丁类名和补丁方法
通过classLoader,找到要修复的bug类名及方法
利用hook技术,在native修改指ArtMethod针变量,使其指向补丁方法,从而完成bug修复。
在类加载后,动态修改native指针,修复即时生效,无需冷启动
类已经被加载,内存中方法描述符(结构体)已经固定,所以只能替换,不能做新增修复。
在Native操作指针时,强转ArtMethod的类型是AndFix写死的,无法保证是运行时的ArtMethod结构,这会产生十分严重的兼容问题
实践发现Andfix 修复成功率非常低 ,时常出现崩溃,补丁无效的现象
代表:腾讯系的 Qzone超级补丁(dex插入) Tinker(dex替换)
增量Dex
Hook ClassLoader.pathList.dexElements[]
将补丁的dex插入到数组的最前端。
ClassLoader的findClass是通过遍历dexElements[]中的dex来寻找类的。所以会优先查找到修复的类。从而达到修复的效果。
Vm的判定规则:“当一个类中引用了另外一个类,则一般要求两个类来自同一个Dex文件”。
CLASS_ISPREVERIFIED 是触发Vm判定规则的前提。
增量方案为解决这个问题,需要进行“打桩”。
打桩的目的就是防止类被打上 CLASS_ISPREVERIFIED 标签。
打桩,就是在所有类中分别引用另外一个独立Dex文件(为了打桩特意封装的)中的类。通常做法是在每一个类中增加构造器并引用另外一个dex中的类。
在类加载的最后阶段,虚拟机会对未打上 CLASS_ISPREVERIFIED 标签的类 再次进行 校验和优化 ,如果在同一时间点加载大量类,那么就会出现严重的性能问题,如启动时白屏。
不需要考虑对dalvik虚拟机和art虚拟机做适配
代码是非侵入式的,对apk体积影响不大
需要下次启动才修复
性能损耗大,为了避免类被加上 CLASS_ISPREVERIFIED,使用插桩,单独放一个帮助类在独立的dex中让其他类调用。可能导致严重的性能问题,如启动时白屏。
全量Dex替换
为了避免dex插桩带来的性能损耗,dex替换采取另外的方式(整体替换dex)。
提供dex差量包(只包含patch代码的dex)
将patch.dex与应用的classes.dex合并成一个完整的dex
加载完整dex得到dexFile对象作为参数构建一个Element对象
整体替换掉旧的dex-Elements数组
相比 dex插入,dex替换的优点
减少了dex插桩带来的性能损耗
Dex合并内存消耗在虚拟机堆内存(vm heap)上,容易OOM,最后导致合并失败
底层替换存在不同定制Rom的兼容性问题,同时不能做新增field的修复,但修复立即生效。
类加载方案在合成全量补丁的时候存在性能问题,修复需要重启应用(冷启动),但是兼容性较好。
Sophix对类文件修复 采用底层替换方案为主,类加载方案为次(兜底策略)的模式,将二者结合起来,并对二者另辟蹊径,加以突破。
底层替换方案通过在运行时利用hook操作native指针实现“热”的特性。但这里有一个关键点,底层替换所操作的指针,实际上是 ArtMethod 。
在类被加载,类中的每个方法都会有对应的ArtMethod,它记录了方法包括所属类和内存地址信息
Andfix正是通过篡改ArtMethod,将补丁方法ArtMethod的成员值逐一赋给旧方法,实现替换。
问题就出现在 逐一替换 上。因为Andfix的 ArtMethod 方法结构是根据Android开源代码写死的,面对国内厂商的定制,经常会导致两者ArtMethod方法结构不一致,这也是兼容问题产生的根本原因。
为了解决这个问题,Sophix采用了对旧ArtMethod进行 完整替换。
通过动态测量ArtMethod的size(通过c层的mempy(dest ,src ,size)方法),进行全量拷贝。这样做无论ArtMethod被修改成什么样,只需要统一执行拷贝,就可以完成替换,完全无视修改虚拟机导致的ArtMethod结构差异。
底层替换虽能使修复即时生效,但由于类加载后,方法结构已固定,这就造成使用上会有诸多限制。
相反类加载方案的使用场景更为广泛。
Sophix使用类加载作为兜底方案。在热部署无法使用的情况下,自动降级为冷部署方案。
无论是冷部署还是热部署,都需要通过同一套补丁兼顾。
在Art虚拟机下,默认支持多dex加载,虚拟机会优先加载命名为classes.dex的文件。
Sophix利用了这一点,将补丁文件命名为classes.dex,并对原有dex文件进行排序。这样一来,art虚拟机就会先加载补丁文件,后续加载的同类名的类会被忽略,最后将加载得到的dexFile把dexElements整体替换。
Dalvik默认只加载classes.dex,其他dex则被忽略。
Sophix就需要一个全量dex。
tinker采用自主研发的dexDiff技术,从方法和指令的维度进行dex合成,但Dex合成过程发生在虚拟机堆内存上,修复的成功率极大的受到性能问题的影响。
为了解决这个问题,Sophix换了一种思路,从类的维度,对照补丁包中出现的类,在原有包中做删除操作。为了避免删除整个类信息而导致dex结构发生偏移,所以只对旧包中类的入口进行删除,实际上类的信息还在dex包中。这样一来,冷启动后,原有的类就不会被加载,相比Tinker的合成方案,Sophix的思路更为轻量化。
至此,Sophix对类文件修复的基本原理描述完毕。
可以说Sophix吸取了百家之长,对问题的解决之法堪称巧妙,展现出底层技术的重要性,若没有对虚拟机等底层技术的深耕探索,在系统框架的纷繁规则面前,也只能至于庭前止步。