摘要:
热修复领域充斥着各大流派,如阿里AndFix、美团Robust、QQ空间、微信Tinker等,每种方法各有优劣。本文所介绍的阿里Hotfix 2.x是在1.x版本进行了优化和创新,不仅支持灵活切换热部署和冷部署的方案;同时,实现了资源、SO文件、类修复的实时生效;整体接入过程采用傻瓜式方法,完全不侵入打包过程,为用户提供了可视化的UI界面。
在阿里HotFix2.0升级详解直播中,阿里HotFix核心开发工程师悟二从热修复背景、常见的热修复方案、阿里HotFix历程及2.0的突破与创新三个方面展开了详细的演讲。分享中,他重点介绍了阿里Hotfix2.X 类、SO文件、资源文件修复方案以及管理后台服务,并对阿里Hotfix 2.X将来需要新增的功能也做了展望。
热修复背景
正常Bug修复流畅包括版本上线、用户安装、发现Bug、紧急修复、重新发版、用户安装六步。该流程中存在着明显的不足,首先重新发布版本代价太大,由于APK是通过多渠道发布的,重新发版时,需要再次对每个渠道推送新的APK包;其次,用户下载安装成本也随之增高;此外,由于需要重新发版、安装,导致Bug修复不及时,用户体验较差。
热修复流程的前四步同正常修复流程相同,但在热修复中,不再进行重新发版,取而代之的是生成上传补丁;此后,SDK拉取加载补丁,完成Bug修复。在热修复过程中,无需重新发版,能够实时高效热修复;同时实现了用户无感知修复,无需下载新的应用,代价小。相比于正常修复过程,热修复的成功率更高,将损失降到了最低。
热修复的几大流派
常见的热修复流派包括阿里AndFix、美团Robust、QQ空间、微信Tinker等,下面来一一介绍。
阿里AndFix
阿里andfix 热修复方案通过Hook本地方法. 并没有整体替换class,整体流程如下:第一步,打开链接库得到操作句柄, 获取native层内部函数, 得到classobject对象;第二步,修复访问权限属性为Public ;第三步,得到新旧方法的指针,新方法指向目标方法,实现方法的替换。
整个过程中不侵入打包,性能无损耗;同时可以即时生效。其缺点同样明显:兼容性方面很不稳定,需要针对Dalvik虚拟机和Art虚拟机做适配;不支持新增类方法/字段,以及修改方法,也不支持对资源的替换;运行时方法被Patch,有Crash风险。
美团Robust
美团热修复方案Robust 的原理类似Instant Run,每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码。客户端拿到Patch.Dex后,用DexClassLoader加载Patch.Dex。其中的changeQuickRedirect字段赋值为用Patch.Dex中的StatePatch.java这个Class New出来的对象。
在整个打Patch过程中,该方案正常的使用DexClassLoader,兼容性高;未反射注入,能够实时生效。该方案的缺点在于:因为在每端函数前插入代码,需要侵入打包过程;原来能被ProGuard内联的函数不能被内联了,所以可能导致方法数的增加,可能会超过65536限制,同时也会导致APK体积增大;该方案不支持SO文件和资源文件的修复。
手机QQ空间
手机QQ空间热修复方案可以用注入和插桩概括。其大致过程是:把Bug方法修复之后,放到一个单独的DEX内,插入到Dex Elements数组的最前面,让虚拟机去加载修复完后的方法。
该方案类似谷歌的Multidex ,在保障稳定性的前提下兼容性很高。缺点是:不支持实时生效;在Davilk下,类加载存在性能问题;Art下,补丁包涵有类、父类以及引用该类的所有类,因此补丁包较大;由于原DEX中的类需要引用额外的DEX类,需要侵入式打包。
微信Tinker
微信Tinker为了解决QQ空间补丁技术由于插桩带来的效率问题,引入DEX差量包。其主要的原理与QQ空间超级补丁技术基本相同,最大区别在于:不再将Patch.dex增加到Elements数组中,而是差量的方式给出Patch.Dex;然后将Patch.Dex与应用的Classes.Dex合并,然后整体替换掉旧的DEX,达到修复的目的。
该方案中通过自研DexDiff算法,深度利用Dex的格式来减少差异的大小,从而做到补丁包足够小。其缺点在于:不支持实时生效;由于补丁DEX需要和原DEX合并,需要占用额外内存和磁盘空间,并且很容易因为内存消耗等原因合并失败;与QQ空间补丁技术相同,同样需要侵入式打包。
上图是Hotfix和主流的热修复方案的效果对比。可以看出,在即时生效、性能消耗、Rom体积、接入复杂度、补丁包大小、类替换、SO文件替换、资源方案等方面,Hotfix都具有相对的优势。
阿里Hotfix 1.x版本
阿里Hotfix 1.x在AndFix的基础上,增加了补丁管理后台;同时基于手淘的实践,针对AndFix做了大量优化, 性能上提高了兼容和稳定性;功能上支持新增类并提供了更小的补丁包(这是因为基于类方法作为粒度)。
从图中可以看到阿里Hotfix 1.x服务后台功能,用户可以新建应用版本,然后根据版本号上传补丁;此外,还提供了补丁控制功能,比如停止发布、继续发布、灰度/全量发布等功能。
但阿里Hotfix 1.x仍存在很多限制:
不支持资源、So文件修复;不支持新增类方法/类字段,这是因为Hotfix 1.x本质上是Hook一个已存在的的方法;
参数包括Long、Double、Float基本类型的方法不能被Patch,同时参数超过8的方法不能被Patch;
被反射调用的方法不能被Patch,具体来说是非静态方法的反射调用会提示IllegalArgumentException 异常,当静态方法被反射调用,如果反射调用不涉及类对象,则可以被Patch;
构造方法不能被Patch,实际上不允许修改一个类字段(包括静态的和非静态的);
正在运行的方法不能被Patch,也就是说如果一个方法正在运行,然后方法的在Native层的结构被替换, 那么就很可能导致Crash。
阿里Hotfix2.0方案
相比于1.X版本,阿里Hotfix 2.x版本将上面的限制完全取消,不仅仅只基于AndFix,而是灵活切换热部署和冷部署的方案;实现了资源、SO文件、类修复的实时生效,同时采用了傻瓜式接入方案,完全不侵入打包过程,对用户提供了可视化的UI界面打补丁。
阿里Hotfix 2.x版本由于相比于1.x版本,变更很大,,因此需要预先做大规模的集成测试才会正式上线。
阿里Hotfix2.X 类修复方案
Hotfix2.X在热修复过程中是不侵入打包过程的,而是通过补丁工具生成补丁。由于热部署Andfix修复正在运行的方法有Crash的风险, 所以补丁工具提供参数由业务方来决定是否尝试走热部署;如果用户Patch的方法没有被高频调用同时又有实时生效的需求,那么可以优先选择走热部署方案;但这并非绝对,当代码变更导致热部署不支持时,还是会转向冷部署。
热部署
热部署就是AndFix支持的代码变更,此时走优化后的AndFix方案,也就是Hotfix1.X方案。
冷部署
冷部署就是AndFix不支持的代码变更。冷部署针对Davilk和Art分别做了不同的处理:
- Davilk下,注入追加到PathClassLoader的dexElements,但无需不插桩,通过Hack本地方法从而绕过dvmresolveclass;
- Art下直接合成一个完整dex,采用手淘目前成熟的art动态部署方案,最后替换PathClassLoader的dexElements即可。
阿里Hotfix2.X SO文件修复方案
- Davilk和ART下SO文件加载的方式不一样,导致了需要区分Art和Davilk做不同的处理:
- ART下预Load原来的SO文件,再加载补丁SO文件;
- Davilk下预Load补丁SO文件,再加载原来的SO文件。
这里的关键是:综合机型支持的Abis和补丁包中的Abis共同决定补丁SO的新LibPath。这两种加载方式都需要对加载两次SO文件,势必会增加一次本地内存的消耗,因此为了达到更好的性能,在Hotfix2.X中提供了下面两个接口替换掉系统加载SO文件的接口:
- SOPatchManager.load(String libPath) 代替 System.load(String pathName)
- SOPatchManager.loadLibrary(String libName)代替 System.loadLibrary(String libName)
阿里Hotfix2.X 资源文件修复方案
在资源文件中,资源ID编码于Resources.arsc文件中,排布紧密,并按照排布顺序进行自动编号;RES目录保存所有带ID的资源文件。布局文件为二进制形式的XML文件,XML以资源ID的方式引用其他资源;Assets目录存放所有原始文件,不带ID;Aapt进行资源的构造,包括自动分配资源ID与R文件的生成,默认情况下,每次编译不保证和之前包中的ID一致。
目前市面上普遍采用的三种资源修复方案:
- 差量合成完整的资源包,运行时完整加载资源;这种方案的缺点是:合成资源占用时间和内存,容易引起卡顿。
- 修改aapt,对以后可能新增的资源提前留空,运行时Patch包中新增资源ID对应留出的位置;该方案的缺点是:需改变打包流程,修改代码并编译替换SDK中的Aapt;打包侵入太强,且留空占用一定磁盘空间。留空多少是预先定好的,无法改变。
- 插件化,组件化资源;这种方式的缺点是:资源需要划分模块,提前规划。
百川资源文件修复方案直接基于新旧两个APK来构造补丁包,不需要改造AAPT,对编译过程无要求;同时,精确比较各个资源ID的使用情况,最大程度利用原先基线包资源,补丁包中只包含新增和修改的资源;在运行时无需合成操作,快速应用生效,不影响性能。
百川资源文件修复方案不仅仅是简单修复,对于任意程度、乃至天翻地覆的修改都能适用,但补丁文件会比较大,该方案兼容Android所有机型,只需选取新旧两个APK,一键快捷生成补丁,并且稳定性较好;配合类修复方案,能够做到资源修复的实时生效。在使用百川资源文件修复方案,需要注意以下地方:
(1)如果事先自己做了资源混淆,需要保证新旧包混淆的关系保持一致,否则打补丁时会找不到原来基线包中资源,而将非新增资源视为新增资源,导致补丁包变大。
(2)建议每次打包时设置去除无用的资源。这样即可以减小包大小,同时也保证补丁包中新增资源都是有用的。
(3)AndroidManifest中引用的资源无法改变。有些资源如icon是安装时固定的,目前所有补丁方案都无法进行改变。而另一些资源,如Theme,我们可以提取AndroidManifest中的资源信息,通过代码的方式进行设置。
阿里Hotfix管理后台服务
阿里Hotfix后台目前提供的服务主要有:补丁灰度发布/正式发布、补丁回滚和补丁安全。
- 补丁灰度发布/正式发布,在发布前可以通过本地或扫码两种方式验证之后再发布上线; 本地补丁模式是指补丁可以放到任何一个指定的目录下即可;扫码模式是扫描二维码生成一个下载URL,然后直接下载,此时不需同服务器验证身份;灰度发布指定具体的用户数然后随机推送。
- 补丁回滚是指发生错误时可以回滚到目标补丁版本,同时该应用版本下的所有设备都会回滚到目标补丁的版本。
- 补丁安全方面,HotFix后台托管了RSA密钥,同在在补丁加载时需要进行安全签名校验。
在不远的将来未来,将在阿里HotFix平台上推出以下服务:
- 补丁自定义平台无关AES秘钥。在打补丁时,用户可以自定义AES密码,然后在SDK初始化时填入这个秘钥即可。阿里百川平台对该秘钥完全无感知,做到补丁在后台的绝对安全。
- 补丁条件下发,支持分系统版本、分渠道以及自定义TAG的方式下发补丁到目标位置。
- 实时显示补丁加载成功率等数据,后续可能会上报补丁加载失败详情, 方便排查问题。
- 一键清除补丁,使用回滚功能必需要具备两个条件:①当前的版本已停止发布;②- 该版本之前存在至少一个历史版本。所以如果第一个补丁就下发错误的话,补丁回滚就无能为力了,所以需要提供一键清除补丁的功能。
在Swift Hot Patch上的进展
在Swift 3推出之后,也是有越来越多的项目转向了Swift。那么,在业界技术分享言必称Swift的时代,Hot Patch对Swift的支持情况又是怎样呢?
目前业界已有两种成熟的产品:WAX和JSPatch。前者是基于Lua语言的Patch方案;后者是基于JavaScript语言的Patch方案。两者都依托于Objective-C的Runtime,通过Method Swizzling将待Patch方法的实现替换为_objc_msgForward/_objc_msgForward_stret,再替换forwardInvocation:为特定语言的桥接方法来调用补丁实现。
由于两者都强依赖Objective-C的Runtime和NSObject的forward机制,它们在支持Swift Hot Patch时就会受到相应约束。
相比于WAX和JSPatch,Rollout.io是更面向Swift的Hot Patch方案。它在编译器swiftc前增加了代码注入逻辑,修改swift相关的代码。这样,swiftc在编译时使用的就是经过注入的代码了。这里以viewDidLoad方法为例,说明代码的注入:首先,根据方法ID取出Patch所需的数据;接着,判断是否应该Patch;如果应该Patch,则以Patch数据、target、方法入参和方法调用closure为参数执行Patch。经过注入,Patch就可以替代任何方法的原始实现。
然而,Rollout.io的实现有很多问题:它支持的Patch非常受限,只能patch class instance function;此外,它并不支持在Patch中调用Swift方法,调用Objective-C方法时,它的语法也非常繁琐。
HotFix的Swift Hot Patch融合了WAX/JSPatch和Rollout.io的优长,并补足了Rollout.io在Patch方面的短板。具体来说,除了在class function patch时没有WAX/JSPatch和Rollout.io的诸多限制外,也同时支持global function、struct function和enum function的Patch。在调用Objective-C方法时,维持了简短的语法;同时,也正在研究动态调用Swift方法的方式。
SteveMcConnell在《CodeComplete》中曾说过“Program into your language, not in it”。希望Swift Hot Patch工具能够帮助更多的开发者深入Swift,*地使用语言去表达自己的思想,而不只是停留在语法层面。