作者:赵裕, 腾讯移动客户端开发
商业转载请联系腾讯 WeTest 获得授权,非商业转载请注明出处。
原文链接:http://wetest.qq.com/lab/view/338.html
本文探讨了 Android 热修复技术的发展脉络,现状及其未来。
热修复技术在近年来飞速发展,尤其是在 InstantRun 方案推出之后,各种热修复技术竞相涌现。国内大部分成熟的主流 APP 都拥有自己的热修复技术,像手淘、支付宝、QQ、饿了么、美团等等。
目前能搜集到的资料,大多简单罗列每个方案的特点并进行横向比较,而其中技术发展的脉络往往被掩盖了。热修复技术从何而来,又将往何处去?在这些资料中都找不到答案。
我认为,走马观花地看一遍各家的热修复方案并不能找到答案,所以写下本文,希望从一个不同的角度来了解热修复技术,权当抛砖引玉,如有不足,欢迎指正。
代码热修复是最常见,也是热修复中最重要的部分,因为程序错误往往都是代码逻辑的错误。最初的热修复方案也仅支持代码热修复。代码热修复分两个流派,即腾讯系的类加载方案和阿里系的底层替换方案,前者需要重启应用但却能修复大部分错误,后者及时生效却只能作方法内的修改。下面详细介绍。
1、Qzone
Qzone 的超级热修复方案是业界最早的热修复方案之一,原理简单而巧妙,影响深刻而久远,在此简单介绍。Android 类加载的源码如下:
可以看出当有多个 dex 文件时,他们会组成一个有序数组,按顺序加载,而对于一个已经加载的 Class 是不会再次加载的,由此得出热修复方案:把需要修复的类打包成一个 dex 文件下发,并在 APP 启动时通过反射,将这个 dex 文件放在 dexElements 的最前面,这样修复了的 Class 就会比有 Bug 的 Class 优先加载了。如下图所示:
但在实现过程中,会遇到 unexpected DEX problem 异常,Qzone 方案为了解决这个问题采用了插桩的策略来规避这个异常。实际上,Android 系统的检查和优化都是有其意义的,因此这种方法在 Dalvik 和 Art 上都会遇到问题。
● 在 Dalvik 虚拟机,APP 在安装的时候会被执行 dexopt 操作,同一个 dex 文件内的 Class 会被打上 CLASS_ISPREVERIFIED 标志,而补丁包中的类并没有打上此标志,因此抛出异常。解决方法就是在第一次打包 APK 时让所有类都引用另一个 dex 文件中的类,这样所有的类始终不会打上 CLASS_ISPREVERIFIED 标志,因此补丁包可以顺利加载,但是 Dalvik 虚拟机在检测到一个类未打上 CLASS_ISPREVERIFIED 之后会再次在类加载的时候进行 dexopt 相关的操作,如果一次性加载很多类,速度将明显变慢。
● 在 Art 虚拟机,dex 文件最终会编译成本地机器码,在 dex2oat 时 fast * 已经将各个类的地址写死,若补丁包中的类出现字段或者方法的修改,会出现内存地址错乱,解决办法是将这个类的父类和调用这个类的类都加入补丁包。但这样会导致补丁包急剧增大。(实际上要理解清楚这个问题需要熟悉 Dalvik 和 Art 的完整流程,并非三言两语能解释清楚)
这两个问题都可以解决,但都要付出一些代价:类加载速度或者补丁包大小。
2、Tinker
如果 Qzone 没有上面两个缺陷,或许就不会有 Tinker 了。对于微信这样一个对性能有极高要求的产品来说,Qzone 的缺点会被无限放大。在参考 Instant Run 的冷插拔与 buck 的 exopackage 后,Tinker 采用了全量替换的策略。全量替换可以避免插桩和地址写死问题,但是补丁包会很大,因此可以在新旧两个 Dex 的差异放在补丁包中,下发到移动端后再在本地合成完整的 dex 文件。
实际上,Tinker 保留了 Qzone 最核心的东西:反射修改 dexElements。无论是插入还是替换,本质都是利用了类加载的特点。由于需要下发的全量补丁包体积过大,Tinker 采用了后台求 diff,下发 diff 文件,移动端合成全量包的策略。
如果仅此而已,只要有 diff/patch 算法,就可以开发 Tinker 了。实际上,确实如此。而 Tinker 第二个创新之处就是采用了自研的 DexDiff 算法,大大优化了下发差异包的大小。
阿里的 Andfix 热修复方案是底层替换方案的代表,与 Qzone 和 Tinker 的思想完全不同,Andfix 通过修改一个方法的入口地址来达到修复。以 Dalvik 虚拟机为例,Andfix 的核心代码如下:
其实就是修改了方法包括入口地址在内的每一项数据的地址,使之指向一个新的方法。在后台,使用 Andfix 提供的 apkpatch 工具,可以得到补丁文件 out.apatch,这个文件记录了哪些方法需要修改,以及修改后的方法。Andfix 效果:
(注意我一直在点击,下发补丁后发生了变化……)
除了代码热修复,资源热修复也很常见。各大主流方案在资源修复的实现上大多参考了 InstantRun 的实现方式,因此本章节先讨论了 InstantRun,再分析了基于 InstantRun 所实现的热修复。
InstantRun 在 AndroidStudio2.0.0 中引入。
InstantRun 包括代码修复和在资源修复,资源修复的核心代码:
其实做了两件事:
关键是要熟悉 Android 相关源码,才能确定哪些字段是需要更新引用的。通过以上两步即可实现资源替换。
将 InstantRun 的 monkeyPatchExistingResource 方法引入我们的代码就可以实现资源热修复,效果如下:
so 库的修复本质是对 native 方法的修复和替换,和类加载方案类似,可以把补丁 so 库的路径插入到 nativeLibraryDirectories 数组的最前面,使得优先加载补丁库而不是原来的库来达到修复目的。在此不做赘述。
最初 Qzone 就需要在 Dalvik 平台进行插桩,Tinker 同样也是分平台合成(在 Dalvik 平台合成全量 Dex,在 Art 平台合成需要的小 Dex),而阿里的 Andfix 作为底层热修复方案,不仅要面对两种虚拟机平台,甚至要为不同 Android 版本编写一套替换逻辑,如下:
加载了补丁包的程序本质还是未编译的程序,只是两个已编译程序的结合体,由于 Java 的编译过程对于我们是透明,所以我们一不小心就会引入错误,而且这种错误十分隐蔽。在使用类加载方案时由于还是在 Java 层,可能不那么容易犯错,但使用 Andfix 等底层热修复方案时却总是防不胜防。
比如,Java 在编译匿名内部类时会编译成顶级类,命名方式为 ClassName$n,其中 n 为匿名内部类出现的顺序,所以在第 i 个匿名内部类前面添加匿名内部类就会导致 ClassName$i#methodName 变成 ClassName$i+1#methodName,即一个方法的地址发生改变。再比如,Java 的泛型编译可能会在编译期引入新的方法,也会导致 Andfix 的异常。
因为编译过程是透明的,所以热修复后的程序不能代替修复问题后重新编译出来的程序,即热修复后程序的安全性是得不到保证的。
Qzone 时期插桩影响了类加载的速度,Tinker 的 DexDiff 算法粒度过细、实现复杂,导致性能消耗严重,Andfix 使用场景有限、兼容性差,此外美团的 Robust、饿了么的 Amigo 等也都各有限制。Android 热修复技术虽然百花齐放,但却并没有哪种方案能够解决所有问题,统一当前的局面。而最近阿里又推出了 Sophix,针对各种类型的修复又做了深度的优化,虽然没有开源代码,但是发布了《深入探索 Android 热修复技术原理》,引起 Android 社区的关注,其统一各种热修复方案的意图也十分明显。
从 Qzone 到 Tinker,从 Andfix 到 Sophix 都可以看出来,热修复技术还在不断上升发展,每一次新方案的推出都是对原有方案的超越。但目前来看,阿里并未打算开源 Sophix,而 Tinker2.0 仍然在路上,热修复技术在性能、兼容、开发透明方面仍然有很多不足,所以不能仅仅满足于了解已有方案,还要深入源码去理解原理,更要对业界最新进展保持关注。
参考:
1、安卓 App 热补丁动态修复技术介绍
2、微信 Tinker 的一切都在这里,包括源码 (一)
3、alibaba/AndFix: AndFix is a library that offer hot-fix for Android App.
4、Instant Run: How
5、微信 Android 热补丁实践演进之路
6、Tencent/tinker: Tinker is a hot-fix solution library for Android, it ……
腾讯 WeTest 兼容性测试团队积累了 10 年的手游测试经验,旨在通过制定针对性的测试方案,精准选取目标机型,执行专业、完整的测试用例,来提前发现游戏版本的兼容性问题,针对性地做出修正和优化,来保障手游产品的质量。目前该团队已经支持所有腾讯在研和运营的手游项目。
iPhone8/iPhoneX 新机即将同步上线,欢迎进入:http://wetest.qq.com/product/expert-compatibility-testing 使用专家兼容测试服务。 WeTest 兼容性测试团队期待与您交流!You Create,We Test!
如果对使用当中有任何疑问,欢迎联系腾讯 WeTest 企业 qq:800024531