阿里Sophix热修复

阿里巴巴对Android热修复技术已经进行了长达多年的探索。

最开始,是手淘基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术,Dexposed。但这个方案由于对底层Dalvik结构过于依赖,最终无法继续兼容Android5.0以后ART虚拟机,因此作罢。

后来支付宝提出了新的热修复方案Andfix。Andfix同样是一种底层结构替换的方案,也达到了运行时生效即时修复的效果,并且重要的是,做到了Dalvik和ART环境的全版本兼容。阿里百川结合手淘在实际工程中使用Andfix的经验,对相关业务逻辑解耦后,推出了阿里百川Hotfix方案,并得到了良好的反响。

此时的百川Hotfix已经是一个很不错的产品了,对于基本的代码修复需求都可以解决,安全性和易用性都做的比较好。然而,它所依赖的基石,Andfix本身,是有局限性的。且不说其底层固定结构的替换方案稳定性不好,其使用范围也存在着诸多限制,虽然可以通过改造代码绕过限制来达到相同的修复目的,但这种方式既不优雅也不方便。而更大的问题是,Andfix只提供了代码层面的修复,对于资源和so的修复都还未能实现。

再看一下同期的其他热修复方案,此时的热修复技术可谓是百花齐放,微信的Tinker、QQ空间的Nuwa、饿了么的Amigo、美团的Robust等等,各个热修复方案争相发布,都声称自己可以做到全方位全功能的热修复。不过他们各自有自身的局限性,或者不够稳定,或者补丁过大,或者效率低下,或者使用起来过于繁琐,大部分技术上看起来似乎可行,但实际体验并不好。而在我们看来,有很多技术细节能够做得更加完美。

终于在2017年6月11日,手淘技术团队联合阿里云正式发布了新一代Android移动热修复方案——Sophix

Sophix的横空出世,将会打破各家热修复技术纷争的局面。我们可以满怀信心地说,在Android热修复的三大领域:代码修复、资源修复、so修复方面,以及方案的安全性和易用性方面,Sophix都做到了业界领先。

设计理念

Sophix的核心设计理念,就是非侵入性。

我们的打包过程不会侵入到apk的build流程中。我们所需要的,只有已经生成完毕的新旧apk,而至于apk是如何生成的——是Android Studio打包出来的、还是Eclipse打包出来的、或者是自定义的打包流程,我们一律不关心。在生成补丁的过程中间既不会改变任何打包组件,也不插入任何AOP代码,我们极力做到了——不添加任何超出开发者预期的代码,以避免多余的热修复代码给开发者带来困扰。

在Sophix中,唯一需要的就是初始化和请求补丁两行代码,甚至连入口Application类我们都不做任何修改,这样就给了开发者最大的透明度和自由度。我们甚至重新开发了打包工具,使得补丁工具操作图形界面化,这种所见即所得的补丁生成方式也是阿里热修复独家的。因此,Sophix的接入成本也是目前市面上所有方案里最低的。

这种非侵入式热更新理念,是我们在设计过程中从用户使用角度进行了深入思考而提炼出的核心思想。

这里的用户,指的自然是广大的开发者。对于开发者而言,热修复应该是一个与业务无关的SDK组件,在整个开发过程中感知不到它的存在。最理想的情况,就是开发者拿过来两个apk,一个是已经安装在手机上的apk,另一个是将要发布出去的apk。我们直接通过工具,就可以根据这两个apk生成补丁,然后把这个补丁下发给已经安装的旧app上,就可以直接加载,使旧app重生为新的app。而这个加载了补丁包新app,在功能和使用上,将会和直接安装新apk别无二致。

至于Sophix这个名字,是来源于Sophic(明智的)+ FIX,一个更明智的热修复方案。

详细比较

下面的这张表格,从几个热修复最重要的维度,把Sophix和另外两个主要商业化热修复方案进行了比较。

方案对比SophixTinkerAmigo
DEX修复 同时支持即时生效和冷启动修复 冷启动修复 冷启动修复
资源更新 差量包,不用合成 差量包,需要合成 全量包,不用合成
SO库更新 插桩实现,开发透明 替换接口,开发不透明 插桩实现,开发透明
性能损耗 低,仅冷启动情况下有些损耗 高,有合成操作 低,全量替换
四大组件 不能新增 不能新增 能新增
生成补丁 直接选择已经编好的新旧包在本地生成 编译新包时设置基线包 上传完整新包到服务端
补丁大小
接入成本 傻瓜式接入 复杂 一般
Android版本 全部支持 全部支持 全部支持
安全机制 加密传输及签名校验 加密传输及签名校验 加密传输及签名校验
服务端支持 支持服务端控制 支持服务端控制 支持服务端控制

可以看到,Sophix在各个指标上全面占优。而其中唯一支持不完善的地方就是四大组件,四大组件可以修改代码,但是无法做到新增。这是因为如果要新增四大组件,必须在AndroidManifest里面预先插入代理组件,并且尽可能声明所有权限,而这么做就会给原先的app添加很多臃肿的代码,对app运行流程的侵入性很强,所以,本着对开发者透明与代码极简的原则,我们没有做这种多余的处理。

直接看表格的话,其中有些技术细节可能还看不太明朗,那么接下来,我将从各个角度,深度解读Sophix的技术优势以及它与同类技术的差别。

技术分析

Sophix的诞生,起初是对原先的阿里百川Hotfix 1.X版本进行升级衍进。

原先百川Hotfix服务端的整套请求控制流程,以及安全检查这部分,是与热修复功能相对分离的,因此我们依旧保留了这部分的逻辑。

而原本的热修复方案,主要限制在于Andfix本身,我们最开始也是从突破原先修复限制入手,希望能够基于原先的Andfix代码做一些必要的改进。然而最终发现,Andfix自身限制几乎是无法绕过的,在运行时对原有类的结构是已经固化在内存中的,它的一些动态属性和很难进行扩展。并且由于Android系统的碎片化,厂商的虚拟机底层结构都不是确定的,因此直接基于原先机制进行扩展的风险很大。

所以我们绕开了具体的技术实现细节,直接从修复的原理入手,对原先的代码修复技术进行深挖和改良。

回顾为期九个多月的探索与开发,这其中无处不体现着我们对易用性和优雅性的极致追求,在技术先进性与易用性上我们达到了完美的平衡。所以,当我们再回头看目前市面上的其他热修复技术,真的有一种“曾经沧海难为水”的感觉。

代码修复

代码修复有两大主要方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案。

这两类方案各有优劣:

  • 底层替换方案限制颇多,但时效性最好,加载轻快,立即见效。
  • 类加载方案时效性差,需要重新冷启动才能见效,但修复范围广,限制少。

底层替换方案。

底层替换方案是在已经加载了的类中直接替换掉原有方法,是在原来类的基础上进行修改的。因而无法实现对与原有类进行方法和字段的增减,因为这样将破坏原有类的结构。

一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常地索引到正确的方法了。如果字段发生了增加和减少,和方法变化的情况一样,所有字段的索引都会发生变化。并且更严重的问题是,如果在程序运行中间某个类突然增加了一个字段,那么对于原先已经产生的这个类的实例,它们还是原来的结构,这是无法改变的。而新方法使用到这些老的实例对象时,访问新增字段就会产生不可预期的结果。

这是这类方案的固有限制,而底层替换方案最为人诟病的地方,在于底层替换的不稳定性。

传统的底层替换方式,不论是Dexposed、Andfix或者其他安全界的Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。例如,改Dalvik方法的jni函数指针、改类或方法的访问权限等等。这样就带来一个很严重的问题,由于Android是开源的,各个手机厂商都可以对代码进行改造,而Andfix里ArtMethod的结构是根据公开的Android源码中的结构写死的。如果某个厂商对这个ArtMethod结构体进行了修改,就和原先开源代码里的结构不一致,那么在这个修改过了的设备上,通用性的替换机制就会出问题。这便是不稳定的根源。

而我们也对代码的底层替换原理重新进行了深入思考,从克服其限制和兼容性入手,以一种更加优雅的替换思路,实现了即时生效的代码热修复。我们实现的是一种无视底层具体结构的替换方式,

也就是把原先这样的逐一替换 
andfix_replace_artmethod

变成了这样的整体替换 
my_replace_artmethod

这么一来,我们不仅解决了兼容性问题,并且由于忽略了底层ArtMethod结构的差异,对于所有的Android版本都不再需要区分,代码量大大减少。即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列,就能直接适用于将来的Android 8.0、9.0等新版本,无需再针对新的系统版本进行适配了。事实也证明确实如此,当我们拿到Google刚发不久的Android O(8.0)开发者预览版的系统时,hotfix demo直接就能顺利地加载补丁跑起来了,我们并没有做任何适配工作,鲁棒性极好。

具体技术细节,可以看这篇文章:Android热修复升级探索——追寻极致的代码热替换

类加载方案

类加载方案的原理是在app重新启动后让Classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新类。从而达到热修复的目的。

再来看看腾讯系三大类加载方案的实现原理。QQ空间方案会侵入打包流程,并且为了hack添加一些无用的信息,实现起来很不优雅。而QFix的方案,需要获取底层虚拟机的函数,不够稳定可靠,并且有个比较大的问题是无法新增public函数。

微信的Tinker方案是完整的全量dex加载,并且可谓是将补丁合成做到了极致,然而我们发现,精密的武器并非适用于所有战场。Tinker的合成方案,是从dex的方法和指令维度进行全量合成,整个过程都是自己研发的。虽然可以很大地节省空间,但由于对dex内容的比较粒度过细,实现较为复杂,性能消耗比较严重。实际上,dex的大小占整个apk的比例是比较低的,一个app里面的dex文件大小并不是主要部分,而占空间大的主要还是资源文件。因此,Tinker方案的时空代价转换的性价比不高。

其实,dex比较的最佳粒度,应该是在类的维度。它既不像方法和指令维度那样的细微,也不像bsbiff比较那般的粗糙。在类的维度,可以达到时间和空间平衡的最佳效果。基于这个准则,我们另辟蹊径,实现了一种完全不同的全量dex替换方案。

我们采用的也是全量合成dex的技术,这个技术是从手淘插件化框架Atlas汲取的。我们会直接利用Android原先的类查找和合成机制,快速合成新的全量dex。这么一来,我们既不需要处理合成时方法数超过的情况,对于dex的结构也不用进行破坏性重构。

cold_dex_merged

从图中可以看到,我们重新编排了包中dex的顺序。这样,在虚拟机查找类的时候,会优先找到classes.dex中的类,然后才是classes2.dex、classes3.dex,也可以看做是dex文件级别的类插桩方案。这个方式十分巧妙,它对旧包与补丁包中classes.dex的顺序进行了打破与重组,最终使得系统可以自然地识别到这个顺序,以实现类覆盖的目的。这将会大大减少合成补丁的开销。

双剑合璧

既然底层替换方案和类加载方案各有其优点,把他们联合起来不是最好的选择吗?Sophix的代码修复体系正是同时涵盖了这两种方案。两种方案的结合,可以实现优势互补,完全兼顾的作用,可以灵活地根据实际情况自动切换。

这两种方案我们都进行了重大的改进,并且从补丁生成到应用的各个环节都进行了研究,使得二者能很好地整合在一起。在补丁生成阶段,补丁工具会根据实际代码变动情况进行自动选择,针对小修改,在底层替换方案限制范围内的,就直接采用底层替换修复吗,这样可以做到代码修复即时生效。而对于代码修改超出底层替换限制的,会使用类加载替换,这样虽然及时性没那么好,但总归可以达到热修复的目的。

另外,运行时阶段,Sophix还会再判断所运行的机型是否支持热修复,这样即使补丁支持热修复,但由于机型底层虚拟机构造不支持,还是会走类加载修复,从而达到最好的兼容性。

资源修复

目前市面上的很多资源热修复方案基本上都是参考了Instant Run的实现。实际上,Instant Run的推出正是推动这次热修复浪潮的主因,各家热修复方案,在代码、资源等方面的实现,很大程度上地参考了Instant Run的代码,而资源修复方案正是被拿来用到最多的地方。

简要说来,Instant Run中的资源热修复分为两步:

  1. 构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中。这样就得到了一个含有所有新资源的AssetManager。
  2. 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager。

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

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

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

  1. 不修改AssetManager的引用处,替换更快更完全。(对比Instanat Run以及所有copycat的实现)
  2. 不必下发完整包,补丁包中只包含有变动的资源。(对比Instanat Run、Amigo等方式的实现)
  3. 不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比Tinker的实现)

所以,我们不要被所谓的“官方实现”束缚住手脚,其实Instant Run的开发团队和Android framework的开发团队并不是同一个团队,他们对于Android系统机制的理解未必十分深入。只要你认真研读系统代码,实现一个比官方更好的方案绝非难事。所以我想说的是,要想实现技术方案的突破,首先就需要破除所谓“权威”的观念。

资源修复的更多技术细节,可通过这篇文章一探究竟:Android热修复升级探索——资源更新之新思路

so库修复

so库的修复本质上是对native方法的修复和替换。

我们知道JNI编程中,native方法可以通过动态注册和静态注册两种方式进行。动态注册的native方法必须实现JNI_OnLoad方法,同时实现一个JNINativeMethod[]数组,静态注册的native方法必须是Java+类完整路径+方法名的格式。

native-regist-method

动态注册的native方法映射通过加载so库过程中调用JNI_OnLoad方法调用完成,静态注册的native方法映射是在该native方法第一次执行的时候才完成映射,当然前提是该so库已经load过。

我们采用的是类似类修复反射注入方式。把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库,而不是原来so库的目录,从而达到修复的目的。

insert-patch

采用这种方案,完全由Sophix在启动期间反射注入patch中的so库。对开发者依然是透明的。不用像某些其他方案需要手动替换系统的System.load来实现替换目的。

未来展望

热修复的必要性

热修复是一个与业务完全无关的模块,开发者如果要自己实现一套可靠的热修复框架,将花费大量时间和精力。虽然市面上已经有很多开源的热修复实现,然而其中的很多坑,往往要踩过才知道,等你把这些坑一一踩过之后,可能大量的用户已经对你失去信心。所以,依靠一个稳定可靠、而且简单实用的商业版本,反而能使各方面的成本降到最低。并且,热修复并不是简单的客户端SDK,它还包含了安全机制和服务端的控制逻辑,这整条链路也不是短时间内可以快速完成的。

还是那句老话,专业是事交给专业的人去做。开发者应该把更多时间精力放到自己的核心业务之中。

Sophix提供了一套更加完美的客户端服务端一体的热更新方案。做到了图形界面一键打包、加密传输、签名校验和服务端控制发布与灰度功能,让你用最少的时间实现最强大可靠的全方位热更新。并且在代码修复、资源修复、SO库修复方面,都做到了业界最佳。

对Android的生态的影响

很多人会把热修复技术跟其他国内厂商的“黑科技”混为一谈。有人说,你们国内开发者就是瞎搞,就不能给我们Android用户一个更加纯净的环境吗?

这里我需要澄清一下。热修复技术不同于其他国内的Android“黑科技”。就比如,国内Android进程保活,是让app持续驻留在后台避免被系统杀死,这既耗费手机电量又占内存,浪费了很多手机资源。再比如,app自行定制的推送服务,无节操地对用户进行信息轰炸。还有更过分的全家桶,一个app同时拉起一票app,并且长期占着内存,使得手机卡顿不堪。总归,这些技术都是为了app厂商的利益而损害手机使用者的实际体验。

而热修复技术是完全不同的,它达到的是一个手机用户和开发者双赢的目的。不仅厂商可以快速迭代更新app,使得功能能最快上线。并且由于热更新过程是毫无感知的,手机用户也减少了繁琐的更新步骤,节省了大量等待更新的时间。这实际上是改善了Android的生态环境。只是这其中最重要的,是要保证热修复功能的稳定性。而Sophix的稳定性,是经过了无数开发者检验的,并且还有手淘多年深厚的技术沉淀作为保障。

Android与iOS热修复的不同

前段时间,苹果封杀了iOS的热修复功能,很多人就因此不看好热修复技术了。这里我想说的是,苹果的政策并不能证明他有多先进,相反,作为独裁者,苹果做过很多不得人心的事,就比如前段时间封杀微信的文章打赏功能。热修复功能被禁止,会使得很多app不得不靠直接发版进行更新,这样一旦新版本出了问题,整个更新迭代过程变得十分漫长。并且一些试验性功能无法进行灰度,这就使得一个重要功能的更新将直接全量发版,如果功能不够稳定,波及范围就变得非常广。而且,用户需要重新下载整个app,不仅流程漫长,原本不到1MB的补丁就能解决的事,现在不得不下载几十MB的完整包才能更新。

再看回Android的情况,Android热修复和iOS是有极大不同的。主要有两个方面:

  1. 谷歌和苹果在中国的地位不同
  2. Android和IOS的开放性不同

谷歌在中国没有像苹果那样的控制力,即使它想要封杀也不可能,国内是有各个安卓应用市场的,没有统一的app安装渠道。另外,Android是开源的,各个厂商都可以做定制,想统一各家的安装渠道几乎是不可能的。

未来,无限可能!

我们对于未来是很乐观的,Android的热修复领域不仅不会受到封杀,反而还有很大的发展空间。我们正在尝试支持各大加固厂商,目前阿里聚安全修复已经支持了Sophix,热修复结合安全加固,将会使得app的稳定性和安全性更加坚不可摧。甚至后续还可以与系统厂商合作,对系统app乃至系统组件进行修复,这样就可以避免频繁OTA升级。

因此,热修复所能发挥是价值将是十分巨大的。热修复还可以与其他领域进行碰撞,引发无限的可能性。在这里,我们欢迎所有应用厂商以及ROM厂商与我们合作,共同使得Android的生态更加完善。

posted @ 2018-10-15 21:49  linghu_java  阅读(951)  评论(0编辑  收藏  举报