阿里巴巴对Android热修复技术已经进行了长达多年的探索。
最开始,是手淘基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术,Dexposed。但这个方案由于对底层Dalvik结构过于依赖,最终无法继续兼容Android5.0以后ART虚拟机,因此作罢。
后来支付宝提出了新的热修复方案Andfix。Andfix同样是一种底层结构替换的方案,也达到了运行时生效即时修复的效果,并且重要的是,做到了Dalvik和ART环境的全版本兼容。阿里百川结合手淘在实际工程中使用Andfix的经验,对相关业务逻辑解耦后,推出了阿里百川Hotfix方案,并得到了良好的反响。
此时的百川Hotfix已经是一个很不错的产品了,对于基本的代码修复需求都可以解决,安全性和易用性都做的比较好。然而,它所依赖的基石,Andfix本身,是有局限性的。且不说其底层固定结构的替换方案稳定性不好,其使用范围也存在着诸多限制,虽然可以通过改造代码绕过限制来达到相同的修复目的,但这种方式既不优雅也不方便。而更大的问题是,Andfix只提供了代码层面的修复,对于资源和so的修复都还未能实现。
再看一下同期的其他热修复方案,此时的热修复技术可谓是百花齐放,许多产品都声称自己可以做到全方位全功能的热修复。不过他们各自有自身的局限性,或者不够稳定,或者补丁过大,或者效率低下,或者使用起来过于繁琐,大部分技术上看起来似乎可行,但实际体验并不好。而在我们看来,有很多技术细节能够做得更加完美。
终于在2017年6月11日,手淘技术团队联合阿里云正式发布了史上首个非侵入式移动热更新解决方案——Sophix。
Sophix入口:https://www.aliyun.com/product/hotfix
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和另外两个主要商业化热修复方案进行了比较。
可以看到,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结构体进行了修改,就和原先开源代码里的结构不一致,那么在这个修改过了的设备上,通用性的替换机制就会出问题。这便是不稳定的根源。
而我们也对代码的底层替换原理重新进行了深入思考,从克服其限制和兼容性入手,以一种更加优雅的替换思路,实现了即时生效的代码热修复。我们实现的是一种无视底层具体结构的替换方式,也就是把原先这样的逐一替换 :
变成了这样的整体替换 :
这么一来,我们不仅解决了兼容性问题,并且由于忽略了底层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的结构也不用进行破坏性重构。
手淘插件化框架Atlas下载:
https://github.com/alibaba/atlas
从图中可以看到,我们重新编排了包中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方法映射通过加载so库过程中调用JNI_OnLoad方法调用完成,静态注册的native方法映射是在该native方法第一次执行的时候才完成映射,当然前提是该so库已经load过。
我们采用的是类似类修复反射注入方式。把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库,而不是原来so库的目录,从而达到修复的目的。
采用这种方案,完全由Sophix在启动期间反射注入patch中的so库。对开发者依然是透明的。不用像某些其他方案需要手动替换系统的System.load来实现替换目的。
未来展望
热修复的必要性
热修复是一个与业务完全无关的模块,开发者如果要自己实现一套可靠的热修复框架,将花费大量时间和精力。虽然市面上已经有很多开源的热修复实现,然而其中的很多坑,往往要踩过才知道,等你把这些坑一一踩过之后,可能大量的用户已经对你失去信心。所以,依靠一个稳定可靠、而且简单实用的商业版本,反而能使各方面的成本降到最低。并且,热修复并不是简单的客户端SDK,它还包含了安全机制和服务端的控制逻辑,这整条链路也不是短时间内可以快速完成的。
还是那句老话,专业是事交给专业的人去做。开发者应该把更多时间精力放到自己的核心业务之中。
Sophix提供了一套更加完美的客户端服务端一体的热更新方案。做到了图形界面一键打包、加密传输、签名校验和服务端控制发布与灰度功能,让你用最少的时间实现最强大可靠的全方位热更新。并且在代码修复、资源修复、SO库修复方面,都做到了业界最佳。
对Android的生态的影响
很多人会把热修复技术跟其他国内厂商的“黑科技”混为一谈。有人说,你们国内开发者就是瞎搞,就不能给我们Android用户一个更加纯净的环境吗?
这里我需要澄清一下。热修复技术不同于其他国内的Android“黑科技”。就比如,国内Android进程保活,是让app持续驻留在后台避免被系统杀死,这既耗费手机电量又占内存,浪费了很多手机资源。再比如,app自行定制的推送服务,无节操地对用户进行信息轰炸。还有更过分的全家桶,一个app同时拉起一票app,并且长期占着内存,使得手机卡顿不堪。总归,这些技术都是为了app厂商的利益而损害手机使用者的实际体验。
而热修复技术是完全不同的,它达到的是一个手机用户和开发者双赢的目的。不仅厂商可以快速迭代更新app,使得功能能最快上线。并且由于热更新过程是毫无感知的,手机用户也减少了繁琐的更新步骤,节省了大量等待更新的时间。这实际上是改善了Android的生态环境。只是这其中最重要的,是要保证热修复功能的稳定性。而Sophix的稳定性,是经过了无数开发者检验的,并且还有手淘多年深厚的技术沉淀作为保障。
Android与iOS热修复的不同
前段时间,苹果封杀了iOS的热修复功能,这给iOS的开发者带来了很大困扰。
热修复功能被禁止,会使得很多app不得不靠直接发版进行更新,这样一旦新版本出了问题,整个更新迭代过程变得十分漫长。并且一些试验性功能无法进行灰度,这就使得一个重要功能的更新将直接全量发版,如果功能不够稳定,波及范围就变得非常广。而且,用户需要重新下载整个app,不仅流程漫长,原本不到1MB的补丁就能解决的事,现在不得不下载几十甚至上百MB的完整包才能更新。
苹果这一政策的推出,使得很多人也因此不看好Android的热修复技术了。在这里,我们可以打消这种错误的观念。因为Android的情况和iOS是有极大不同的。主要有两个方面:
1. 谷歌和苹果在中国的地位不同
2. Android和iOS的开放性不同
谷歌在中国没有像苹果那样的控制力,即使它想要封杀也不可能,国内是有各个安卓应用市场的,没有统一的app安装渠道。另外,Android是开源的,各个厂商都可以做定制,想统一各家的安装渠道几乎是不可能的。
未来,无限可能
我们对于未来是很乐观的,Android的热修复领域不仅不会受到封杀,反而还有很大的发展空间。我们正在尝试支持各大加固厂商,目前阿里聚安全修复已经支持了Sophix,热修复结合安全加固,将会使得app的稳定性和安全性大大提高,更加坚不可摧。甚至后续还可以与系统厂商合作,对系统app乃至系统组件进行修复,这样就可以避免频繁OTA升级。
因此,热修复所能发挥是价值将是十分巨大的。热修复还可以与其他领域进行碰撞,引发无限的可能性。在这里,我们非常期待携手广大应用厂商以及ROM厂商,共同推动Android的生态更加完善。