以下内容根据演讲视频以及PPT整理而成。
视频分享链接,点击这里!
在传统的修复模式下,如果线上的App出现Bug之后进行修复所需要的时间成本非常高,这是因为往往需要发布一个新的版本,然后将其发布到对应的应用商城中,然后通知用户下载和更新自己的App。但是尤其在Android这样没有统一应用市场的环境下,修复周期可能需要以周来计数。而随着App的业务越来复杂、代码量越来越大,出现Bug的概率也会越来越高,如果继续按照传统的修复模式就很难满足业务发展的需求。正是因为这样的现状,很多Android端开发的同学就开始思考是否有一种在不发版的前提下修复线上Bug的技术,也就是所谓的热修复技术。通过这几年的发展,热修复技术也得到了很大的发展,很多公司也具有了比较成熟的热修复方案,同时这些热修复方案也大规模地在生产环境中进行了实践。本次会选取几个比较具有代表性的热修复方案与大家分享。
在本次的分享中主要会讲到三种技术方案:Qxxx(化名)方案、Instant Run和Sophix。Qxxx属于比较早期的修复方案,而现在由于种种原因可能不会成为大家在进行热修复时的首选方案,但是其技术原理给了我们很大启发,所以在本次的技术分享中将Qxxx方案放在第一个进行介绍。第二种方案叫做Instant Run,这是所有从事Android开发的同学都比较熟悉的一种方案,严格意义上来讲Instant Run其实不算是一个热修复的方案,它仅仅是作为Android Studio来提高开发效率的一个功能而已,但是其背后的技术原理却和很多修复方案具有很多相通的地方。第三种方案叫做Sophix,它是阿里巴巴刚刚发布的一种修复方案,它代表着新一代的热修复方案,Sophix的功能更加强大而且能够覆盖的场景也非常广,所以也是一个比较优秀的方案。
一、Qxxx方案解析
1.1 Qxxx方案原理介绍
1.2 Android端的类加载原理
接下来为大家简单介绍Android端类的加载原理。其实Android的类加载和Java的类加载比较类似,都是通过ClassLoader类加载器进行加载,唯一的区别就是Android类加载器加载的是dex文件,所以在Android中加载器的基类叫做BaseDexClassLoader,这个类之下会有两个子类,一个叫做PathClassLoader,它负责加载Android的SDK,当代码中引用到Android框架的本身类的时候都是通过PathClassLoader进行加载的;而另外一个子类叫做DexClassLoader,这个类就是用于加载业务层面代码的加载器。
1.3 类预校验问题
而实际情况中,做这样的方案是不行的。直接进行替换就会出现预校验的问题。
1.4 字节码注入
针对预校验问题,有同学可能会认为只需要在写代码的时候引用一些类就可以了,但是这在实际情况下却是非常困难的,因为本身Android在打包代码的时候就会尽可能地将相互依赖的类打包在同一个dex里面,所以依靠打包方案本身是很难解决这个问题的。但是预校验问题也不是没有办法解决的,解决的思路是当这些类已经被编译完成之后,在字节码的层面去注入一些来自于其他dex的类。
1.5 代码插桩
但是为什么慢慢地大家开始觉得Qxxx方案并不好呢?其原因就在于插桩并不是一个非常好的方式,它所带来的开销是非常大的。在dex opt的过程中会执行一个验证的过程,再执行一个优化的过程,最后将dex文件转成odex文件。因为进行了插桩,所有的类都没有被打上预校验的标签,所以验证和优化这两个过程会被放在真正类加载的时候去执行,如果一两个类在运行的时候进行加载和优化对于App的性能的影响不大,但是现在的App越来越复杂,当有成千上万的类需要在运行时进行加载和优化的时候,所带来的开销就是非常可观的了。
二、Instant Run方案解析
从严格意义上来讲,Instant Run其实并不算一个热修复方案,它只是一个优化开发效率的机制。在传统的开发模式中,当在开发的过程中对代码进行了一些改动就会进行全量的构建,然后将一个完整的App部署到测试机上,之后进行应用重启,然后就可以看到代码的变化与运行效果的变化。
2.1 Instant Run打包逻辑
- manifest注入;大家都知道一个Android工程的所有组件都会注册到manifest文件下,在这部分中,Instant Run会生成一个自己的application,然后将这个application注册到manifest配置文件里面去,也就是说当整个App运行起来的时候,首先执行的就是application这个类,也就是运行的是Instant Run本身的框架,它可以去做一系列准备工作,当这些工作完成之后再去运行业务代码。
- Instant Run代码放入主dex;manifest注入之后,会将Instant Run的代码放入到Android虚拟机第一个加载的dex文件中,包括classes.dex和classes2.dex,这两个dex文件存放的都是Instant Run本身框架的代码,而没有任何业务层的代码。正是因为以上的原因,当整个App运行起来的时候首先执行的都是Instant Run的代码。
- 工程代码插桩——IncretmentalChange;这个插装里面会涉及到具体的IncretmentalChange类。
- 工程代码放入instantrun.zip;这里的逻辑是当整个App运行起来之后才回去解压这个包里面的具体工程代码,运行整个业务逻辑。
- 当bootstrap application启动之后会首先加载classes.dex和classes2.dex这两个主dex文件,当这两个主dex文件启动之后,就会启动AppServer服务。这里可以将AppServer理解为一个服务器,它会与IDE也就是Android Studio建立连接。当连接建立之后,后续在开发的过程中的代码改动所形成的补丁包都会通过这个连接下发到App上,并且通过AppServer接收,再通过相应的处理使得补丁生效。
- 当完成了第一个步骤之后,会用本身的ClassLoader去加载instantrun.zip包里面真正的工程代码。
- 最后一步,将宿主application替换成真实的realApplication,然后真正地运行自定义application里面的逻辑,达到隐藏自身的效果。
2.2 Instant Run热插拔、温插拔和冷插拔简介
当App启动之后会启动一个AppServer服务器的连接,当它加载到patch之后会去判断patch是否能够进行热插拔、温插拔和冷插拔,然后再去做各种方式所对应的事情。
- HotSwap(热插拔):修改方法实现后代码可以实时生效,不需要重启App也不需要重启activity,只要加载补丁之后就可以马上生效。通常情况下,热插拔只适用于方法体内部的逻辑改变。
- WarmSwap(温插拔):主要针对于需要修改或删除资源的情况。温插拔不需要重启App,但是需要重启当前的activity后才能生效。
- ColdSwap(冷插拔):主要针对于改变了类的结构、继承关系、实现接口等情况,此时因为类结构本身被改变了,需要重新去加载这个类,所以需要重启App之后才能生效。
2.3 Instant Run热插拔(HotSwap)原理解析
首先IDE下发patch,加载到补丁之后,在App层Instant Run的框架会通过AppPatchLoader去找到哪些类需要被修复,当找到需要被修复的类之后再通过反射的手段将类中的$change变量设置为已经修复后的类。这样当执行MainActivity的onClick方法的时候实际上执行到的是MainActivity&override的onClick方法,从而实现了热修复。
2.4 Instant Run温插拔(WarmSwap)原理解析
对于温插拔而言,需要首先简单介绍一下资源修复的逻辑。其实对于Android框架比较熟悉的同学都清楚,在每一个activity里面都会有一个叫做mResource的变量,这个mResource变量指向一个Resource对象。在Resource对象中会存在一个指向AssetManager的mAsset变量,而AssetManager类才是真正去管理和维护所有对于资源的访问的具体类。AssetManager类里面会有两个具体成员,一个是framework-res.apk,其是系统自带的资源,另一个则是App本身的资源,而所有对于资源的访问最终都会走到AssetManager类中。正是因为这样的机制,Instant Run就是通过替换AssetManager的方式达到资源修复的效果。
2.5 Instant Run冷插拔(ColdSwap)原理解析
之前提到所有的用户代码都被写到instantrun.zip包里面,当代码结构本身发生了变化之后,可以在把对应的代码补丁下发到App之上的时候,将对应的patch写入对应的Instant Run的路径底下,再重新进行dex opt的过程,之后框架就可以加载对应的类了。
2.6 Instant Run方案总结
- 首先,热插拔的优势在于其不需要重启,只需要代码补丁被下发到端上之后就可以实时地看到修复效果。但是热插拔的劣势也很明显,因为使用了插桩,所以其性能的开销会非常大。
- 其次,对于温插拔而言,它的优势是可以实现资源修复,但是其劣势就是这种方案会下发全量资源包,开销也是非常大的。
- 最后,对于冷插拔而言,它支持完整类的替换,但是也存在分包的限制,必须要去做切片,当修改了某一个类之后需要把这个类所有所属的dex类都打成一个新的dex然后下发到端才可以。
三、Sophix方案解析
Sophix是阿里巴巴刚刚推出的一款无侵入的热修复方案,本次分享中就为大家揭晓Sophix的神秘面纱,看看它到底是怎样实现的。
3.1 Sophix及时修复(Andfix)原理解析
Sophix也是支持及时修复的,在这一点上与Instant Run一样,对于方法体逻辑的修改可以在App不重启的情况下进行。Sophix的及时修复方案其实早在阿里曾经开源的Andfix方案里面就已经实现了。
大家都知道所有的类被加载之后,其方法都会被放在方法区,这是Java层面的概念,其实这些方法区在native层也就是C层面都会有各自对应的结构体来描述对应的方法以及执行的逻辑。如果某一个类的方法出现了Bug,那么可以去新建一个类,把修复后的方法放到这个类里面,同时把原来那个类的方法的指针指向新方法的方法体就可以实现方法体的替换,从而实现热修复的效果。
虽然Andfix及时修复方案看上去很美好,也很漂亮,既能够实现及时修复又没有使用插桩付出性能上的代价,但是这个方案也存在很大的限制。之前提到了任何虚拟机在native层都会有对应的结构体来描述方法,而在不同的虚拟机上,描述方法的结构体都是不一样的,所以需要针对不同的Android虚拟机版本去做不同的适配来匹配不同的结构体,这样一来兼容性的操作就会非常多。大家都很清楚Android端各个厂商都会定义自己的虚拟机,这时候就无法知道方法所对应的结构体内部是什么样的,也就无法实现方法的替换了,所以Andfix方案在现实的环境中存在很大的限制,兼容性会受到非常大的挑战。
但是,虽然可以使用整体复制的方式去做一次性的结构体替换,但是前提是必须要知道方法结构体的尺寸大小,只有在知道这些之后才能进行替换,这也就是第二个难题,因为不同的虚拟机版本的结构体大小也不同,那么如何去知道结构体大小呢?对于这一点Sophix方案的解决方法也非常巧妙。
总之,Sophix及时修复的方法是非常巧妙的,既没有用到插桩,同时又不需要考虑兼容性,在性能层面和兼容性层面都具有很好的保障。从及时修复的角度来看,Sophix的确有“四两拨千斤”的功效。
3.2 Sophix冷启动修复原理解析
上面提到的及时修复只能针对方法体内部结构被修改的场景,而对于类本身结构的改变,及时修复就没有办法了,这时候就需要用到冷启动修复。冷启动修复就是需要下发一个新的补丁,在补丁中会有一个新的补丁类,在App重启的时候会优先加载这个补丁类达到去替换原有Bug类的效果。
对于冷启动修复而言,针对于不同的虚拟机有不同的原则,Android主流的Dalvik和ART两个虚拟机,它们最大的区别就是是否支持多个dex文件的加载。ART也就是Android 5.0以上的虚拟机本身就支持多个dex文件加载,而Dalvik却不支持多个dex加载,只支持一个dex加载,如果需要支持多个dex加载则需要引入multi-dex方案。而Dalvik和ART加载多个dex文件的不同却决定了它们需要采用不同热修复方案的原因。
在做冷启动修复的时候,Sophix的根本原则就是非侵入式,不能对于App本身有任何改造,同时也要保证整个App的性能,所以不能使用插桩的方案,也必须要做到dex的全量替换,重新去执行dex opt过程生成新的odex,再去把dexElement数组进行全量替换,达到加载新的补丁的效果。
ART的冷启动修复
ART的冷启动方案是比较简单的,因为ART本身就支持多个dex加载,当然多个dex加载也是存在一定顺序的,首先需要加载classes.dex。正是基于这样的加载顺序,当patch.dex被下发到端上之后,只需要将其放到第一位,也就是将其文件名改为classes.dex,而将原来的文件名依次后移一位,然后重新执行loadDex的加载过程,生成新的odex并全量替换原有的odex,这样就可以保证补丁包dex文件被优先加载,ART下的冷启动修复就是这样实现的。
Dalvik的冷启动修复
下表中除了Sophix还列举了另外两个方案进行对比来看。
在冷启动修复下,Sophix方案有一个很简单的思想就是当发现某些类存在Bug下发新的补丁之后,如果把原有的存在Bug的类从原来所属的dex抠出来再去执行加载的时候,因为原有的dex文件不再有这些类了,此时就会去从patch.dex文件中加载到它,这样就可以实现热修复的效果,并且这样并不会有预检验的问题,从而最大程度地保证了程序性能。所以这个方案中最困难的一点就是如何把以前这些有Bug的类从dex中抠出来,这个问题可能会非常复杂,因为每个类的大小都不一样,如何将其从连续排列的内存空间中取出来然后再去做移位操作,这样想起来很复杂。而实际上Sophix使用了一个很巧妙的方式实现这样的事情。
3.3 Sophix资源修复原理解析
之前在Instant Run里面也提到了资源修复,因为资源修复下发的是全量的资源包,所以并不适合在线上的环境中应用。想要在线上环境做资源修复肯定会使用差量的资源包,下发更新过的资源,而Sophix也是这样做的。
而Sophix的资源修复会涉及到以下三种情况:
- 新增资源导致原有资源id偏移:对比新旧代码前,将新包中所引用的未修改资源ID修正。
- 引用内容修改的资源:对比新旧代码前,在新包中将所引用的原有资源ID置为更新后的ID。
- 删除资源:无需修改。
四、热修复方案的总结和对比
- Qxxx方案原理比较简单,通过代码插桩绕开预校验问题,此外通过dexElement插入的方式使得带补丁的dex文件优先加载。其优点在于实现比较简单,可以修复大部分类层面的问题。但是同时其问题也是比较突出的,第一点是不支持实时生效,第二点就是全量插桩的方式侵入性非常强,同时性能损耗也是非常大的。
- Instant Run方案原理同样用到了代码插桩,而且其插桩比Qxxx方案更加复杂,它还会用到宿主的application做很多准备工作,这之后才会去执行业务代码,最后它还会去通过AssetManager重建做资源修复工作。优点是它能同时支持方法更新、类更新和资源更新,并且在方法更新的过程中还可以做到及时修复,不需要重启App。其问题在于还是使用了全量插桩,所以侵入性很强,同时对于性能的损耗也很大,由于在进行修复时需要下发全量资源包,所以开销非常大,同时也不适合在实际的生产环境中使用。
- Andfix方案的原理是native方法的替换,这个方法很巧妙,并且实现比较简单,而且可以做到及时生效。但是它不支持类结构的改变,同时因为不同版本虚拟机的方法体结构不同,无法实现兼容性的处理,所以这种方案的兼容性也比较差。
- Sophix方案使用了很多很巧妙的原理实现,首先它还是使用native方法替换,这种方法会比Andfix更加巧妙,它不需要知道方法结构体具体的成员变量,而直接使用整体的替换,只需要知道方法结构体的大小即可。它使用了很巧妙的方式,通过两个紧密排列的方法的地址差完成了方法替换即时生效的功能。Sophix使用全量dex替换去完成冷启动修复场景,而在资源修复的时候使用了差量资源包注入的方式,最大限度地降低了网络的开销,只需要很轻量地把差量资源包下发就可以了。其优点在于同时支持方法更新、类更新和资源更新,而且包括native方法的替换以及资源包的注入等很多实现非常巧妙和优雅,也非常轻量,并且属于非侵入式的修复。