2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案、阿里AndFix以及微信Tinker(Bugly sdk也集成Tikner热更新)和阿里最新出品Sophix.它们在原理各有不同,适用场景各异,到底采用哪种方案,是开发者比较头疼的问题。下面是这几种技术方案介绍。
技术背景
一、正常开发流程
从流程来看,传统的开发流程存在很多弊端:
·重新发布版本代价太大
·用户下载安装成本太高
·
BUG修复不及时,用户体验太差
二、热修复开发流程
而热修复的开发流程显得更加灵活,优势很多:
·无需重新发版,实时高效热修复
·用户无感知修复,无需下载新的应用,代价小
·修复成功率高,把损失降到最低
业界热门的热修复技术
—
热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed,腾讯QQ空间的超级补丁和微信的Tinker。最近阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复,所以AndFix技术这块我们重点分析阿里百川HotFix。下面,我们就分别介绍QQ空间超级热补丁技术和微信Tinker以及阿里百川的HotFix技术。
一、QQ空间超级补丁技术
超级补丁技术基于DEX分包方案,使用了多DEX加载的原理,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法
当patch.dex中包含Test.class时就会优先加载,在后续的DEX中遇到Test.class的话就会直接返回而不去加载,这样就达到了修复的目的。
但是有一个问题是,当两个调用关系的类不在同一个DEX时,就会产生异常报错。我们知道,在APK安装时,虚拟机需要将classes.dex优化成odex文件,然后才会执行。在这个过程中,会进行类的verify操作,如果调用关系的类都在同一个DEX中的话就会被打上`CLASS_ISPREVERIFIED`的标志,然后才会写入odex文件。
所以,为了可以正常地进行打补丁修复,必须避免类被打上`CLASS_ISPREVERIFIED`标志,具体的做法就是单独放一个类在另外DEX中,让其他类调用。
修复的步骤为:
1.可以看出是通过获取到当前应用的Classloader,即为BaseDexClassloader
2.通过反射获取到他的DexPathList属性对象pathList
3.通过反射调用pathList的dexElements方法把patch.dex转化为Element[]
4.两个Element[]进行合并,把patch.dex放到最前面去
5.加载Element[],达到修复目的
整体的流程图如下:
从流程图来看,可以很明显的找到这种方式的特点:
优势:
1.没有合成整包(和微信Tinker比起来),产物比较小,比较灵活
2.可以实现类替换,兼容性高。(某些三星手机不起作用)
不足:
1.不支持即时生效,必须通过重启才能生效。
2.为了实现修复这个过程,必须在应用中加入两个dex!dalvikhack.dex中只有一个类,对性能影响不大,但是对于patch.dex来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。
3.在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。
二、微信Tinker
微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。
整体的流程如下:
从流程图来看,同样可以很明显的找到这种方式的特点:
优势:
1.合成整包,不用在构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行。
2.性能提高。兼容性和稳定性比较高。
3.开发者透明,不需要对包进行额外处理。
不足:
1.与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。
2.需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。
3.合并时占用额外磁盘空间,对于多DEX的应用来说,如果修改了多个DEX文件,就需要下发多个patch.dex与对应的classes.dex进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。
三、阿里百川HotFix
阿里百川推出的热修复HotFix服务,相对于QQ空间超级补丁技术和微信Tinker来说,定位于紧急BUG修复的场景下,能够最及时的修复BUG,下拉补丁立即生效无需等待。
1、AndFix实现原理
AndFix不同于QQ空间超级补丁技术和微信Tinker通过增加或替换整个DEX的方案,提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。
原理图如下:
2、AndFix实现过程
对于实现方法的替换,需要在Native层操作,经过三个步骤:
AndFix对ART设备支持,过程与Dalvik相似。
从技术原理,不难看出阿里百川HotFix的几个特点:
优势:
1.
BUG修复的即时性
2.补丁包同样采用差量技术,生成的PATCH体积小
3.对应用无侵入,几乎无性能损耗
不足:
1.不支持新增字段,以及修改方法,也不支持对资源的替换。
2.由于厂商的自定义ROM,对少数机型暂不支持。兼容性差。
(每一个java方法在art种都对应一个ArtMethod, ArtMethod记录了这个java方法的所有信息,包括所属类,访问权限、代码执行地址。
通过evn->FromReflectedMethod,可以由Method对象得到这个方法对应的ArtMethod的真正其实地址,然后就可以把它强转为ArtMethod指针,从而对其所有的成员进行修改。这样就全部替换完之后就完成了热修复逻辑。以后调用这个方法时就会直接走到新方法的实现中了。
然而由于andfix里面的ArtMethod的结构体遵照android虚拟机art源码里面的ArtMethod构建的,各个手机厂商对这个ArtMethod结构体进行修改就会导致喝原来开源代码里面的结构不一致,那么在这个修改过的设备上,替换机制就会出问题,无法正常执行热修复逻辑。)
综合分析如下:
对比总结:
一、qq空间超级补丁,微信Tinker类似多DEX带来的性能影响
我们知道,多DEX方案原来是用于解决应用方法数65k的问题,现在google也官方支持了MultiDex的实现方案。超级补丁技术和Tinker却作为一种热修复的方案,平生给应用增加了多个DEX,而多DEX技术最大的问题在于性能上的坑,因此基于这种方案的补丁技术影响应用的性能是无疑的。
1.启动加载时间过长
我们可以看到,超级补丁技术和Tinker都选择在Application的attachBaseContext()进行补丁dex的加载,即时这是加载dex的最佳时机,但是依然会带来很大的性能问题,首当其冲的就是启动时间太长。
对于补丁DEX来说,应用启动时虚拟机会进行dexopt操作,将patch.dex文件转换成odex文件,这个过程本身非常耗时。而这个过程又要求在主线程中,以同步的方式执行,否则无法成功进行修复。就DEX的加载时间,大概做了以下的时间测试。
通过上表可以看到,随着patch.dex的尺寸增加,在不做任何优化的情况下,启动时间也直线增长。对于一个应用来说,这简直是灾难性的。
2.易造成应用的ANR和Crash
由于多DEX加载导致了启动时间变长,这样更容易引发应用的ANR。我们知道当应用在主线程等待超过5s以后,就会直接导致长时间无响应而退出。超级补丁技术为保证ART不出现地址错乱问题,需要将所有关联的类全部加入到补丁中,而微信Tinker采取一种差量包合并加载的方式,都会使要加载的DEX体积变得很大。这也很大程度上容易导致ANR情况的出现。
除了应用ANR以外,多DEX模式也同样很容易导致Crash情况的出现。在ART设备中为了保证不出现地址错乱,需要把修改类的所有相关类全部加入到补丁中,这里会出现一个问题,为了保证补丁包的体积最小,能否保证引入全部的关联类而不引入无关的类呢?一旦没有引入关联的类,就会出现以下的异常:
·NoClassDefFoundError
·Could Not Find Class
·Could Not Find Method
出现这些异常,就会直接导致应用的Crash退出。
所以,不难看出如果我们需要修复一个不是Crash的BUG,但是因为未加入相关类而导致了更严重的Crash,就更加的得不偿失。
总的来说,热修复本质的目的是为了保证应用更加稳定,而不是为了更强大的功能引入更大的风险和不稳定性。
二、热修复or插件化?
我们经常提到热修复和插件化,这都是当下热门的新兴技术。在讲述之前,需要对这两个概念进行一下解释。
·热修复:当线上应用出现紧急BUG,为了避免重新发版,并且保证修复的及时性而进行的一项在线推送补丁的修复方案。
·插件化:一个程序划分为不同的部分,以插件的形式加载到应用中去,本质上它使用的技术还是热修复技术,只是加入了更多工程实践,让它支持大规模的代码更新以及资源和SO包的更新。
显然,从概念上我们可以看到,插件化使用场景更多是功能上的,热修复强调微小的修复。从这个层面来说,插件化必然功能更加强大,能做的事情也更多。QQ空间超级补丁技术和微信Tinker从类、资源的替换和更新上来看,与其说是热修复,不如说是插件化技术的实践。
QQ空间超级补丁技术和微信Tinker提供了更加强大的功能,但是对应用的性能和稳定有较大的影响,就BUG修复的这个使用场景上还不够明确,并且显得过重。在插件化开发上,有用武之地。
同样andfix兼容又有很大的问题。
终于进入主题阿里最新推出的热修复方案技术sophfix.
Sophix设计理念:
Sophix的核心设计理念,就是非侵入性。
我们的打包过程不会侵入到apk的build流程中。我们所需要的,只有已经生成完毕的新旧apk,而至于apk是如何生成的——是Android
Studio打包出来的、还是Eclipse打包出来的、或者是自定义的打包流程,我们一律不关心。在生成补丁的过程中间既不会改变任何打包组件,也不插入任何AOP代码,我们极力做到了——不添加任何超出开发者预期的代码,以避免多余的热修复代码给开发者带来困扰。
在Sophix中,唯一需要的就是初始化和请求补丁两行代码,甚至连入口Application类我们都不做任何修改,这样就给了开发者最大的透明度和自由度。我们甚至重新开发了打包工具,使得补丁工具操作图形界面化,这种所见即所得的补丁生成方式也是阿里热修复独家的。因此,Sophix的接入成本也是目前市面上所有方案里最低的。
这种非侵入式热更新理念,是我们在设计过程中从用户使用角度进行了深入思考而提炼出的核心思想。
这里的用户,指的自然是广大的开发者。对于开发者而言,热修复应该是一个与业务无关的SDK组件,在整个开发过程中感知不到它的存在。最理想的情况,就是开发者拿过来两个apk,一个是已经安装在手机上的apk,另一个是将要发布出去的apk。我们直接通过工具,就可以根据这两个apk生成补丁,然后把这个补丁下发给已经安装的旧app上,就可以直接加载,使旧app重生为新的app。而这个加载了补丁包新app,在功能和使用上,将会和直接安装新apk别无二致。
Sophfix与其他热修复方案对比:
可以看到,Sophix在各个指标上全面占优。而其中唯一不支持的地方就是四大组件的修复,这是因为如果要修复四大组件,必须在AndroidManifest里面预先插入代理组件,并且尽可能声明所有权限,而这么做就会给原先的app添加很多臃肿的代码,对app运行流程的侵入性很强。
Sophix支持代码修复、资源修复、so库修复。下面对这三种修复进行介绍。
一、代码修复
代码修复有两大主要方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案。
这两类方案各有优劣:
底层替换方案限制颇多,但时效性最好,加载轻快,立即见效。
类加载方案时效性差,需要重新冷启动才能见效,但修复范围广,限制少。
底层替换方案
底层替换方案是在已经加载了的类中直接替换掉原有方法,是在原来类的基础上进行修改的。因而无法实现对与原有类进行方法和字段的增减,因为这样将破坏原有类的结构。
一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常地索引到正确的方法了。
如果字段发生了增加和减少,和方法变化的情况一样,所有字段的索引都会发生变化。并且更严重的问题是,如果在程序运行中间某个类突然增加了一个字段,那么对于原先已经产生的这个类的实例,它们还是原来的结构,这是无法改变的。而新方法使用到这些老的实例对象时,访问新增字段就会产生不可预期的结果。
这是这类方案的固有限制,而底层替换方案最为人诟病的地方,在于底层替换的不稳定性。
传统的底层替换方式,不论是Dexposed、Andfix或者其他安全界的Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。例如,改Dalvik方法的jni函数指针、改类或方法的访问权限等等。这样就带来一个很严重的问题,由于Android是开源的,各个手机厂商都可以对代码进行改造,而Andfix里ArtMethod的结构是根据公开的Android源码中的结构写死的。如果某个厂商对这个ArtMethod结构体进行了修改,就和原先开源代码里的结构不一致,那么在这个修改过了的设备上,通用性的替换机制就会出问题。这便是不稳定的根源。
而我们也对代码的底层替换原理重新进行了深入思考,从克服其限制和兼容性入手,以一种更加优雅的替换思路,实现了即时生效的代码热修复。sophix实现的是一种无视底层具体结构的替换方式,也就是把原先这样的逐一替换:
变成了这样的整体替换:
这么一来,我们不仅解决了兼容性问题,并且由于忽略了底层ArtMethod结构的差异,对于所有的Android版本都不再需要区分,代码量大大减少。即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列,就能直接适用于将来的Android 8.0、9.0等新版本,无需再针对新的系统版本进行适配了。
事实也证明确实如此,当我们拿到Google刚发不久的Android O(8.0)开发者预览版的系统时,hotfix demo直接就能顺利地加载补丁跑起来了,我们并没有做任何适配工作,稳定性极好。
类加载方案
类加载方案的原理是在app重新启动后让Classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新类。从而达到热修复的目的。
再来看看腾讯系三大类加载方案的实现原理。QQ空间方案会侵入打包流程,并且为了hack添加一些无用的信息,实现起来很不优雅。而QFix的方案,需要获取底层虚拟机的函数,不够稳定可靠,并且有个比较大的问题是无法新增public函数。
微信的Tinker方案是完整的全量dex加载,并且可谓是将补丁合成做到了极致,然而我们发现,精密的武器并非适用于所有战场。Tinker的合成方案,是从dex的方法和指令维度进行全量合成,整个过程都是自己研发的。
虽然可以很大地节省空间,但由于对dex内容的比较粒度过细,实现较为复杂,性能消耗比较严重。实际上,dex的大小占整个apk的比例是比较低的,一个app里面的dex文件大小并不是主要部分,而占空间大的主要还是资源文件。因此,Tinker方案的时空代价转换的性价比不高。
其实,dex比较的最佳粒度,应该是在类的维度。它既不像方法和指令维度那样的细微,也不像bsbiff比较那般的粗糙。在类的维度,可以达到时间和空间平衡的最佳效果。基于这个准则,我们另辟蹊径,实现了一种完全不同的全量dex替换方案。
sophix采用的也是全量合成dex的技术,这个技术是从手淘插件化框架Atlas汲取的。直接利用Android原先的类查找和合成机制,快速合成新的全量dex。这么一来,我们既不需要处理合成时方法数超过的情况,对于dex的结构也不用进行破坏性重构。
从图中可以看到,我们重新编排了包中dex的顺序。这样,在虚拟机查找类的时候,会优先找到classes.dex中的类,然后才是classes2.dex、classes3.dex,也可以看做是dex文件级别的类插桩方案。这个方式十分巧妙,它对旧包与补丁包中classes.dex的顺序进行了打破与重组,最终使得系统可以自然地识别到这个顺序,以实现类覆盖的目的。这将会大大减少合成补丁的开销。
双剑合璧
既然底层替换方案和类加载方案各有其优点,把他们联合起来不是最好的选择吗?Sophix的代码修复体系正是同时涵盖了这两种方案。两种方案的结合,可以实现优势互补,完全兼顾的作用,可以灵活地根据实际情况自动切换。
这两种方案我们都进行了重大的改进,并且从补丁生成到应用的各个环节都进行了研究,使得二者能很好地整合在一起。在补丁生成阶段,补丁工具会根据实际代码变动情况进行自动选择,针对小修改,在底层替换方案限制范围内的,就直接采用底层替换修复吗,这样可以做到代码修复即时生效。而对于代码修改超出底层替换限制的,会使用类加载替换,这样虽然及时性没那么好,但总归可以达到热修复的目的。
另外,运行时阶段,Sophix还会再判断所运行的机型是否支持热修复,这样即使补丁支持热修复,但由于机型底层虚拟机构造不支持,还是会走类加载修复,从而达到最好的兼容性。最后也要注意
二、资源修复
不android资源热修复,就是在app不重新安装的情况下,利用下发补丁包直接更新本app中的资源。
目前市面上的资源热修复方案基本上都是参考Instant Run的实现。Instant Run实现过程大概分为两部:
1、构造一个新的AssetManager,并通过反射条用addAssetPath,把这个完整的新资源包加入到AssetManager中。这样就得到了一个含有所有新资源的AssetManager。
2、找到所有之前引用到原油AssetManager的地方,通过反射,把引用处替换为AssetManager
这种方式下发完整的包很占用空间。而像有些方案,是先进行对资源包做差量,在运行时合成完整包再加载。这样确实减少包的体积,但是在运行时多了合成的操作,耗费了运行时间喝内存。合成后的包也是完整的包,仍旧会占磁盘空间。
Sophix采用的一种很巧妙的方式,构造一个package id为0x66的资源包,这个包里面只包含改变了的资源项,然后直接在原来的AssetManager中addAssetPaht这个包。然后,就可以了。忧郁补丁包的package id为0x66和原来文件的package id为0x7f不冲突,因此直接加入到一有的AssetManager中就直接使用了。补丁包里面的资源,只包含原油包里面没有而新包里有的资源以及内容发生改变的资源。
资源的改变包含增加、减少、修改,分别处理方法如下:
1、新增资源,直接加入布丁包
2、减少的资源,我们只要不使用就行了,因此不用考虑这种情况,它不影响布丁包
3、对于修改资源,比如替换了一张图片之类的情况,我们把他视为新增资源,在打入补丁的时候,代码在引用出也会做相应的修改,也就是直接把原来使用的旧资源id的地方变为新id。
Sophix优势:
1.不修改AssetManager的引用处,替换更快更完全。(对比Instanat Run以及所有copycat的实现)
2.不必下发完整包,补丁包中只包含有变动的资源。(对比Instanat Run、Amigo等方式的实现)
3.不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比Tinker的实现)
三、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来实现替换目的。
https://www.jianshu.com/p/0a31d145cad2