由Sophix引发的Android热更新底层原理探索

2,582 阅读10分钟

       移动互联网市场日趋成熟,移动产品研发进入平稳发展阶段,这意味着开发者的思维和研发模式也应转入下半程。安全领域技术在开发中的应用一直是操作系统平台发展周期中的重要一环。热修复,作为安全领域技术的衍生品,自2016年开始,持续受到关注,并不断演进。

        2016年上半年,为了提升产品在敏捷开发下的最佳发布体验,分别尝试了备受关注的阿里和微信两大派系的热更新方案(支付宝的Andfix和微信的Tinker),但在探索的过程中,发现两种方案都存在弊端,如使用场景有限,修复成功率低,存在兼容问题。加之各方案还在内部快速迭代,均未能达到商业化的标准,所以热修复在项目中的应用被暂时搁置了。

        直到最近,看到阿里推出了非侵入式热修复框架Sophix。Sophix对其前辈Andfix,阿里百川Hotfix等方案进行了升级改造,打破了旧方案诸多限制,涵盖了代码修复,资源修复,So库修复。加上阿里云平台的支持,经过简单的配置就可接入使用,目之所及,Sophix已经成为目前成熟度最高的热修复框架。这也让我重新燃起了对热更新及底层技术探索的热情。所以我想以此为契机,用系列文章的形式,围绕热点技术所涉猎的知识进行由浅入深的持续挖掘,用文字记录在学习和探索过程中的所思所想,做到知其然,知其所以然。

热修复的价值

        新技术的层出不穷,适时合理的应用,有助于产品演进并提高生产力。同时,我们应保持冷静,适合自己的才是最好的,避免新技术解决了老问题,却带来更多新问题的尴尬,所以我先从以往应用发布流程入手,通过分析和比较,逐步了解热修复的技术原理,为项目是否引入做参考。常规发布流程如下,


这种发布流程的存在的问题:

1.重新发版,需要重新上架审核,耗时费力

2.用户需要重新下载安装,用户体验差

3.Bug修复不及时,成本高

使用热更新,流程如下


        可见,热更新能够以更低的成本,更灵活的方式应对Bug修复。

        非侵入式一直以来都是框架设计的最佳实践,Sophix同样以此为核心卖点,它不关心APK build过程,只关注修复后的新包,和原有包,通过可视化的补丁工具,生成补丁。这一点极大地降低了接入成本,整体流程如下:


        可以看出,Sophix无论对于开发者还是用户,几乎没有侵入。Sophix团队在文档中提到,他们用巧妙的方式,将低层替换和类加载两个方案联合起来,各取所长。我们知道,将某个框架接入项目时,通常会有官方文档会帮助你完成。但随着框架使用的深入,若总是浮于表面,那么在出现问题时,则会手足无措。加强对底层技术的探索,对程序员来说也是十分有益的。

        前面提到,Sophix是从两种热修复方案中演进,那么我将分别以阿里系低层替换方案和腾讯系的类加载方案为例,从类修复的角度进行分析。

底层替换方案

代表:支付宝的Andfix,话不多说,先上图,


        如图,修复bug后,会构建出一个新的apk,通过Andfix框架提供的工具对比出新旧apk classes.dex文件的差异,并生成patch压缩包,压缩包中比较关键的是PATCH.MF文件和diff.dex,虚拟机通过jarFile读取PATCH.MF文件得到补丁类名,dexFile读取diff.dex,获得补丁方法。使用补丁类名和补丁方法通过classLoader,找到要修复的bug类名及方法。最后利用hook技术(关于hook会在之后的Dalvik文章中详述),在native修改指ArtMethod针变量,使其指向补丁方法,从而完成bug修复。

        底层替换的方案的优点是,在类加载后,动态修改native指针,修复即时生效,无需冷启动。但缺点就是,正因为类已经被加载,内存中方法描述符(结构体)已经固定,所以只能替换,不能做新增修复。同时,在Native操作指针时,强转ArtMethod的类型是AndFix写死的,无法保证是运行时的ArtMethod结构,这会产生十分严重的兼容问题。

        通过实践,发现Andfix修复成功率非常低,时常出现崩溃,补丁无效的现象,经查证,主要还是因为兼容问题。而且不能新增field也是主要问题,新增field会破坏序列,这样一来,调用方法时会造成地址混乱。

类加载方案

同样请先看图


        不得不承认,Tinker中用了很多巧妙的方法。为了减少补丁大小,Tinker需要通过新旧包生成差量包diff.dex。不同于其他类加载方案,Tinker采用全量更新。客户端在获得diff.dex后,会开启一个service进行dex文件的合成,合成的全量dex文件插入到dexElements的第一个位置,dexElements里面的每个Element实际上就是Dex文件,classLoader在启动时,会按顺序加载dex文件,使修复类所在的全量dex包被优先加载,从而完成替换。

        相比较早的增量Dex替换方案(如Qzone超级补丁),Tinker的全量替换很好的规避了触犯Vm的规则:“当一个类中引用了另外一个类,则一般要求两个类来自同一个Dex文件”。增量方案为解决这个问题,需要进行“打桩”,所谓打桩,就是在所有类中分别引用另外Dex文件中的类,通常的做法是在每一个类中增加构造器并引用另外一个dex中的类(这个dex是为了打桩特意封装的)。这样做是防止类被打上CLASS_ISPREVERIFIED标签,CLASS_ISPREVERIFIED是触发Vm判定规则的前提。但是,在类加载的最后阶段,虚拟机会对未打上标签的类‘再次进行校验和优化,如果在同一时间点加载大量类,那么就会出现严重的性能问题,如启动时白屏。

        虽然Tinker用全量替换的方式避免了上述问题,但补丁全量合并的过程发生在虚拟机堆内存上,极易造成OOM,导致修复失败。


        在了解两种热修复方案之后,我们再次总结一下两种方案的优缺点,底层替换存在不同定制Rom的兼容性问题,同时不能做新增field的修复,但修复可以即可生效。类加载方案在合成全量补丁的时候存在性能问题,修复需要重启应用(冷启动),但是兼容性较好。Sophix采用底层替换方案为主,类加载方案为次的模式,将二者结合起来。接下来我们来看看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虚拟机下情况则有些不同,Dalvik默认只加载classes.dex,其他dex则被忽略。那么,Sophix就需要一个全量dex,前面提到tinker采用自主研发的dexDiff技术,从方法和指令的维度进行dex合成,但Dex合成过程发生在虚拟机堆内存上,修复的成功率极大的受到性能问题的影响。为了解决这个问题,Sophix换了一种思路,从类的维度,对照补丁包中出现的类,在原有包中做删除操作,如图。


        为了避免删除整个类信息而导致dex结构发生偏移,所以只对旧包中类的入口进行删除,实际上类的信息还在dex包中。这样一来,冷启动后,原有的类就不会被加载,相比Tinker的合成方案,Sophix的思路更为轻量化。

        至此,对Sophix对类文件修复的基本原理描述完毕。可以说Sophix吸取了百家之长,对问题的解决之法堪称巧妙。但在惊叹阿里技术底蕴的同时,也展现出底层技术的重要性,若没有对虚拟机等底层技术的深耕探索,在系统框架的纷繁规则面前,也只能至于庭前止步。

      仅以此篇作为引导,未来将结合源码,探索更多底层实现细节。