干货满满,Android热修复方案介绍

684 阅读31分钟
原文链接: click.aliyun.com
摘要:在云栖社区技术直播中,阿里云客户端工程师李亚洲(毕言)从技术原理层面解析和比较了业界几大热修复方案,揭开了Qxxx方案、Instant Run以及阿里Sophix等热修复方案的神秘面纱,帮助大家更加深刻地理解了代码插桩、全量dex替换、资源修复等常见场景解决方案,本文干货满满,精彩不容错过。

以下内容根据演讲视频以及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方案原理介绍
fe36f75722f3d5a26f1c58ecabc8f7251495a18e
可能大家对于Qxxx方案比较了解,它也是来自于业界一家非常优秀的公司所提供的方案。首先分享Qxxx方案比较基础的特性,它是基于Android dex分包方案,其次它最关键的技术点在于利用字节码插桩的方式绕开了预校验问题,这也是Qxxx方案最为核心的一点。Qxxx只支持App重启之后才能修复,也就是App在运行的时候加载到了补丁包也不能及时修复,需要App重新启动的时候才会修复,这是因为Qxxx方案是基于类加载区需要重新加载补丁类才能实现的,所以必须进行重启才能修复。此外,Qxxx方案只支持到类结构本身代码层面的修复,不支持资源的修复。

1.2 Android端的类加载原理
接下来为大家简单介绍Android端类的加载原理。其实Android的类加载和Java的类加载比较类似,都是通过ClassLoader类加载器进行加载,唯一的区别就是Android类加载器加载的是dex文件,所以在Android中加载器的基类叫做BaseDexClassLoader,这个类之下会有两个子类,一个叫做PathClassLoader,它负责加载Android的SDK,当代码中引用到Android框架的本身类的时候都是通过PathClassLoader进行加载的;而另外一个子类叫做DexClassLoader,这个类就是用于加载业务层面代码的加载器。
af9318c9418dc12bd7903b62c2a2ba47282d466b
早期在Android端只能加载一个dex文件,后来随着代码越来越多,一个dex文件已经无法存放所有代码了,所以需要加载多个dex文件,这无论是在什么虚拟机内都可以通过相同的手法解决。而解决的关键就是无论有几个dex文件,所有的dex文件最后最后被加入到内存中都会形成一个数组叫做dexElement。而DexClassLoader的原理就是当系统需要加载某一个类的时候,DexClassLoader就会去遍历dexElement数组,当它遍历到某一个元素的时候,在这个元素所对应的文件找到目标类的时候就会停止遍历并及时返回。所以当在不同的dex文件里面出现两个一样的类的时候,哪一个被先遍历到哪个就会被先加载。正是基于这样的原理,Qxxx方案的研发同学就想到如果编写的代码存在Bug,就可以发布一个补丁包,而这个补丁包也是dex文件,并且使这个dex文件能够首先被DexClassLoader加载到,就能够达到修复的效果。所以就是需要通过一定的手段将补丁包的dex文件放在dexElement数组中的第一位,那么就可以首先加载补丁中的类,进而达到修复的效果。

1.3 类预校验问题
而实际情况中,做这样的方案是不行的。直接进行替换就会出现预校验的问题。
33e49660df3fd8d4c14516ef39267ce8fe199610
而预校验问题出现的原因在于当App被安装到设备上之后,会存在一个叫做dex opt的过程,也就是将dex文件进行优化,在优化之后dex文件将会变成odex文件,而在优化过程中就会有一个预校验的问题。当某一个类所有的构造方法、私有方法以及重载方法所引用的其他类和这个类本身都来自于同一个dex文件的时候,这个类就会被打上class_ispreverified标签。所以如果加载的类来自于补丁文件,而补丁文件和之前的文件必然不属于同一个dex,而本身的那个类已经被打上了class_ispreverified标签,但是在运行时又引用了其他dex的类,这样就必然会出现错误。所以Qxxx方案所需要解决的关键问题就是预校验。
49581a82a0c07555e2c4b00ddc906649f1ace69a

1.4 字节码注入
针对预校验问题,有同学可能会认为只需要在写代码的时候引用一些类就可以了,但是这在实际情况下却是非常困难的,因为本身Android在打包代码的时候就会尽可能地将相互依赖的类打包在同一个dex里面,所以依靠打包方案本身是很难解决这个问题的。但是预校验问题也不是没有办法解决的,解决的思路是当这些类已经被编译完成之后,在字节码的层面去注入一些来自于其他dex的类。
620c0cd7d5c65c29d23c7f08a2d13d9dd948cc45
幸运的是Android的gradle插件也提供了这样的一些接口,叫做Transform的API。这个API会提供一个调用的时机,当代码文件被编译成JAR但是还没有被打成dex的时候,提供了在这个时期做一些事情的接口。正是利用这个接口,当拿到编译完成的字节码文件之后,可以对其进行字节码的注入,进行所谓的插桩,插入一些来自于其他dex文件的类,这样当App再被安装并执行dex opt过程的时候就不会再被打上预校验的标签,同时能够成功加载补丁了,这样的方案也是非常巧妙并且有效的。

1.5 代码插桩
但是为什么慢慢地大家开始觉得Qxxx方案并不好呢?其原因就在于插桩并不是一个非常好的方式,它所带来的开销是非常大的。在dex opt的过程中会执行一个验证的过程,再执行一个优化的过程,最后将dex文件转成odex文件。因为进行了插桩,所有的类都没有被打上预校验的标签,所以验证和优化这两个过程会被放在真正类加载的时候去执行,如果一两个类在运行的时候进行加载和优化对于App的性能的影响不大,但是现在的App越来越复杂,当有成千上万的类需要在运行时进行加载和优化的时候,所带来的开销就是非常可观的了。
ef32b7ebaf3dd54a08240a65b94cfc823c1aad9b
曾经有某公司的同学进行过测试,在插桩和不插桩的情况下去比较加载700个类和启动App的情况。实验结论是:在插桩的情况下,700个类的加载时间需要600多毫秒,而在不插桩的情况下只需要80多毫秒,两者相差了近8倍;在启动App的层面,插桩也会明显比不插桩的情况慢了很多。所以可以说插桩所带来的性能开销是非常大的,甚至可以说使用这种方法进行热修复是一种得不偿失的选择,虽然实现了热修复的方案,但是因此丢失了程序良好的性能,也正是因为这个原因,Qxxx方案逐渐被生产环境抛弃了。

二、Instant Run方案解析

从严格意义上来讲,Instant Run其实并不算一个热修复方案,它只是一个优化开发效率的机制。在传统的开发模式中,当在开发的过程中对代码进行了一些改动就会进行全量的构建,然后将一个完整的App部署到测试机上,之后进行应用重启,然后就可以看到代码的变化与运行效果的变化。
f5ce040448c839f7ff095b03bfbd200d4789473a
而随着App越来越复杂,全量构建的过程本身会变得非常耗时,尤其是在需要频繁地进行代码改动观察效果的时候,这就会严重地影响开发效率。基于上述的现状,Android Studio在2.0版本的时候就发布了Instant Run新特性。Instant Run新特性的原理就是当进行代码改动之后,会进行增量构建,也就是仅仅构建这部分改变的代码,并将这部分代码以补丁的形式增量地部署到设备上,然后进行代码的热替换,从而观察到代码替换所带来的效果。其实从某种意义上讲,Instant Run和热修复在本质上是一样的。

2.1 Instant Run打包逻辑
2d6c801f06efcd6b5842b57c99075c2e195c6a32
在接入Instant Run之后,与传统方式相比,在进行打包的时候会存在以下四个不同点:
  1. manifest注入;大家都知道一个Android工程的所有组件都会注册到manifest文件下,在这部分中,Instant Run会生成一个自己的application,然后将这个application注册到manifest配置文件里面去,也就是说当整个App运行起来的时候,首先执行的就是application这个类,也就是运行的是Instant Run本身的框架,它可以去做一系列准备工作,当这些工作完成之后再去运行业务代码。
  2. Instant Run代码放入主dex;manifest注入之后,会将Instant Run的代码放入到Android虚拟机第一个加载的dex文件中,包括classes.dex和classes2.dex,这两个dex文件存放的都是Instant Run本身框架的代码,而没有任何业务层的代码。正是因为以上的原因,当整个App运行起来的时候首先执行的都是Instant Run的代码。
  3. 工程代码插桩——IncretmentalChange;这个插装里面会涉及到具体的IncretmentalChange类。
  4. 工程代码放入instantrun.zip;这里的逻辑是当整个App运行起来之后才回去解压这个包里面的具体工程代码,运行整个业务逻辑。
f29a606ea2d3762966f57380e783b702de6bf743
在App刚开始启动的时候,Instant Run会做以下三件事情:
  1. 当bootstrap application启动之后会首先加载classes.dex和classes2.dex这两个主dex文件,当这两个主dex文件启动之后,就会启动AppServer服务。这里可以将AppServer理解为一个服务器,它会与IDE也就是Android Studio建立连接。当连接建立之后,后续在开发的过程中的代码改动所形成的补丁包都会通过这个连接下发到App上,并且通过AppServer接收,再通过相应的处理使得补丁生效。
  2. 当完成了第一个步骤之后,会用本身的ClassLoader去加载instantrun.zip包里面真正的工程代码。
  3. 最后一步,将宿主application替换成真实的realApplication,然后真正地运行自定义application里面的逻辑,达到隐藏自身的效果。

2.2 Instant Run热插拔、温插拔和冷插拔简介
当App启动之后会启动一个AppServer服务器的连接,当它加载到patch之后会去判断patch是否能够进行热插拔、温插拔和冷插拔,然后再去做各种方式所对应的事情。
17dcb82633d58e88a330b1fb6b5dbcf8da7dc172
这里简单介绍一下在Instant Run里热插拔、温插拔和冷插拔这三个修复方式的概念:
  • 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方法,从而实现了热修复。
a74316341ab3dc96daf6f9ece1e578bd5a44bbb6

2.4 Instant Run温插拔(WarmSwap)原理解析
对于温插拔而言,需要首先简单介绍一下资源修复的逻辑。其实对于Android框架比较熟悉的同学都清楚,在每一个activity里面都会有一个叫做mResource的变量,这个mResource变量指向一个Resource对象。在Resource对象中会存在一个指向AssetManager的mAsset变量,而AssetManager类才是真正去管理和维护所有对于资源的访问的具体类。AssetManager类里面会有两个具体成员,一个是framework-res.apk,其是系统自带的资源,另一个则是App本身的资源,而所有对于资源的访问最终都会走到AssetManager类中。正是因为这样的机制,Instant Run就是通过替换AssetManager的方式达到资源修复的效果。
883868c3916f47fa56e3d0439e8ad869568bd092
具体来说,当资源发生改变需要进行修复的时候,IDE会发布一个资源的补丁发到终端之上,之后Instant Run会新建一个AssetManager。新建的AssetManager里面AppResource的指针就会指向资源的补丁,当指向这个之后再去遍历所有的activity,将其mAssert指针指向新建的AssertManager。这样之后所有的activity指向的都是重现建立的AssertManager,此时只需要去重启activity,那么所有的资源访问就会来自新的资源包里面的资源,这样也就达到了资源修复,也就是温插拔的效果。
676f394512f28e32a463cebf15df9e69b15f97ea
需要注意的是温插拔方案只适用于开发阶段,这是因为这种方案在补丁下发的时候会下发一个完整的全量资源包,如果将这种方案应用于线上就会产生比较大的开销。因为整个App里面大部分都属于资源,如果因为仅仅修复其中的一两个资源就去下发一个完整的资源包,就会造成较大的开销。综上所述,温插拔方案并不适用于整体线上环境,而只是一个开发阶段的优化手段。

2.5 Instant Run冷插拔(ColdSwap)原理解析
之前提到所有的用户代码都被写到instantrun.zip包里面,当代码结构本身发生了变化之后,可以在把对应的代码补丁下发到App之上的时候,将对应的patch写入对应的Instant Run的路径底下,再重新进行dex opt的过程,之后框架就可以加载对应的类了。
098f39f86cd03af08c7bc1ddbb221625e37b6f01
instantrun.zip里面的类都是在运行起来之后才去加载的,这部分的加载是可控的,所以可以进行简单的dex文件替换,之后再去做修复。之所以dex文件会被切成很多片,是因为如果只修改了某个类,只发单一的class补丁放到里面同样会遇到之前所提到的预校验的问题,所以必须要去进行一次dex opt的过程,才可以绕开预校验的问题。

2.6 Instant Run方案总结
3609865fc0e648bc8ad354718c90cb055b36cf08
  • 首先,热插拔的优势在于其不需要重启,只需要代码补丁被下发到端上之后就可以实时地看到修复效果。但是热插拔的劣势也很明显,因为使用了插桩,所以其性能的开销会非常大。
  • 其次,对于温插拔而言,它的优势是可以实现资源修复,但是其劣势就是这种方案会下发全量资源包,开销也是非常大的。
  • 最后,对于冷插拔而言,它支持完整类的替换,但是也存在分包的限制,必须要去做切片,当修改了某一个类之后需要把这个类所有所属的dex类都打成一个新的dex然后下发到端才可以。

三、Sophix方案解析

Sophix是阿里巴巴刚刚推出的一款无侵入的热修复方案,本次分享中就为大家揭晓Sophix的神秘面纱,看看它到底是怎样实现的。

3.1 Sophix及时修复(Andfix)原理解析
Sophix也是支持及时修复的,在这一点上与Instant Run一样,对于方法体逻辑的修改可以在App不重启的情况下进行。Sophix的及时修复方案其实早在阿里曾经开源的Andfix方案里面就已经实现了。

大家都知道所有的类被加载之后,其方法都会被放在方法区,这是Java层面的概念,其实这些方法区在native层也就是C层面都会有各自对应的结构体来描述对应的方法以及执行的逻辑。如果某一个类的方法出现了Bug,那么可以去新建一个类,把修复后的方法放到这个类里面,同时把原来那个类的方法的指针指向新方法的方法体就可以实现方法体的替换,从而实现热修复的效果。
3afab894014229ac128a7c7369fef05f1ca0bfd2
Andfix方案很早就已经开源了,大家如果感兴趣可以去GitHub上拿到源码进行更进一步的分析。总结而言,就是会通过一个工具对于新的和老的两个apk进行diff操作,当diff完成之后就可以找到某一个有被修改了的方法的类,当修改之后就会生成一个新的patch的dex文件,这个文件里面就会有被修改的方法。首先这个类可能会被改名,但是里面会存在一个同名方法,这个方法的注解里面会标明其所替换的方法以及被替换方法所属的类,当这个patch被推送到App端上之后,对应的Andfix的patchManager就会去加载补丁包,首先进行校验,之后加载补丁里面的类,并通过类中的注解去找到它所要替换的类,当两个类都找到之后再将对应的方法找出来,再在native层面对于具体方法的逻辑指针进行替换,从而使得指向原有方法的指针指向新的方法,从而达到及时修复的效果。

虽然Andfix及时修复方案看上去很美好,也很漂亮,既能够实现及时修复又没有使用插桩付出性能上的代价,但是这个方案也存在很大的限制。之前提到了任何虚拟机在native层都会有对应的结构体来描述方法,而在不同的虚拟机上,描述方法的结构体都是不一样的,所以需要针对不同的Android虚拟机版本去做不同的适配来匹配不同的结构体,这样一来兼容性的操作就会非常多。大家都很清楚Android端各个厂商都会定义自己的虚拟机,这时候就无法知道方法所对应的结构体内部是什么样的,也就无法实现方法的替换了,所以Andfix方案在现实的环境中存在很大的限制,兼容性会受到非常大的挑战。
4751f7572a3f2087ae25a9d1036928dd559325db
而在Sophix里面却非常巧妙地解决了上述问题。Sophix方案中提出了一个思想就是不需要去关心方法的结构体内部具体是什么样的,只需要进行一次整体的替换就可以了。原来需要一个成员一个成员地进行遍历替换,现在是整体替换,但是本质是没有区别的。同时也不关心结构体内部要做哪些操作以及每个成员变量所代表的含义,只需要进行整体替换就可以了。这样做有两个好处,一个是比较简单,另一个就是不需要了解虚拟机每个方法体的内部结构,所以理论上对于所有的虚拟机版本都是适用的。

但是,虽然可以使用整体复制的方式去做一次性的结构体替换,但是前提是必须要知道方法结构体的尺寸大小,只有在知道这些之后才能进行替换,这也就是第二个难题,因为不同的虚拟机版本的结构体大小也不同,那么如何去知道结构体大小呢?对于这一点Sophix方案的解决方法也非常巧妙。
fbccc1444a7205d43892f40c16e79f3e55b9628c
大家都知道当一个类被加载的时候,其方法都会被放在方法区,而且同一个类的方法会被紧密地排列在一起,而我们可以拿到两个方法的地址起始值,而这两者之间的差值就是第一个方法结构体的大小。正是基于这一点,只要去构造一个只有两个静态方法的类,那么就可以通过获取这两个方法的起始地址相减拿到方法对应结构体的大小,从而进行整体的替换。

总之,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文件的不同却决定了它们需要采用不同热修复方案的原因。
f427e489effc9e026b256993f3a9f3ba382a325d
ART本身支持多个dex加载,所以在程序启动之前ART已经把多个dex文件都加载进来了,在运行后所有的类都已经被放在ClassLoader里面了,而不需要再去做加载工作。而Dalvik使用的multi-dex方案实际上在程序运行之前只加载了classes.dex文件,而剩下的其他的dex文件都是在程序启动之后application运行的时候再去加载的,所以可以看作在程序运行时进行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下的冷启动修复就是这样实现的。
02fd6fac3465020d5c4ce0797f7eced79a1f14bc
这里进行了一次整体的替换而不再只是某一个dex文件的插入,并且重新执行了dex opt的过程,所以这里不会出现预校验的问题。同时以前和补丁包在同一个dex文件的这些类因为补丁包被打到了新的dex文件中,它们的预检验标签会被去除掉,但是由于这些类的数量很少,所以对于性能的影响也是比较小的,做到了最大程度地降低了热修复所带来的性能开销。而因为loadDex的过程是非常耗时的,所以在真正实现的时候会做通过异步的方式另外开辟一个线程去做dex与odex的转换。当App重新启动,新的odex全部生成之后才会去做dexElement数组的替换,这就最大程度地保证了App运行的稳定性。因为热修复方案的目标是帮助修复问题,所以不能再带来其他的稳定性问题,所以Sophix方案在这里花费了很大精力进行优化。

Dalvik的冷启动修复
下表中除了Sophix还列举了另外两个方案进行对比来看。
7ca769081951973c68d7c0b9add76fd2474df005
这里Qxxx方案原理是dexElement注入,特点是实现比较简单,但是由于基于插桩实现,所以本身类加载的开销比较大,所以不适用于生产环境。Txxx方案和Sophix都是全量地替换dex,同时Txxx也不使用插桩的方式,最大程度地保障了性能,最具有的特点的就是自研了dex合并算法,最大限度地减少了代码体积,而这一点是它的优势也是它的劣势,需要比较细粒度地做dex合并,从而可以把多个dex合并成为一个dex再去加载。但是这个自研的dex合并算法比较复杂,然而带来的收益却并不大,虽然减少了代码体积,但是一个App的体积里面的大部分并不是代码本身而是资源,所以通过合并算法减少的代码体积只是很少的一部分,所以性价比很低。而在Sophix里面,虽然也做了全量的替换,但是相对而言比较简单,以类为粒度合并dex,虽然没有减少dex文件的体积,但是合并的方法更加简单明了。

在冷启动修复下,Sophix方案有一个很简单的思想就是当发现某些类存在Bug下发新的补丁之后,如果把原有的存在Bug的类从原来所属的dex抠出来再去执行加载的时候,因为原有的dex文件不再有这些类了,此时就会去从patch.dex文件中加载到它,这样就可以实现热修复的效果,并且这样并不会有预检验的问题,从而最大程度地保证了程序性能。所以这个方案中最困难的一点就是如何把以前这些有Bug的类从dex中抠出来,这个问题可能会非常复杂,因为每个类的大小都不一样,如何将其从连续排列的内存空间中取出来然后再去做移位操作,这样想起来很复杂。而实际上Sophix使用了一个很巧妙的方式实现这样的事情。
a641e2f1acaeea4042f48d877bee971699d4d7b9
在dex文件中其实会有一个叫做dexHeader的结构,这就是dex文件的文件头,在这里面有两个成员与实现紧密相关:一个叫做classDefsSize,另一个叫做classDefsOff。ClassDef也就是所有的类定义,这个dex文件里面所有涉及到的类都会被注册到classDef的结构体里面,classDefsOff指的就是classDef结构的偏移量,所以通过classDefsOff就可以找到对应的dex文件里面的classDef,从而找到这个dex文件中到底有哪些类,虚拟机也是同样的原理,通过classDefsOff偏移量找到classDef再遍历并加载相应的类。而想要实现热修复只需要在classDef里面将需要被替换的类抹掉就可以了。所以在Dalvik下面的逻辑就是通过DexHeader找到classDefsOff,再在classDefs里面找到需要修改的类,把这些类抹掉再去修改classDefsSize,这样当虚拟机再去加载dex文件的时候就会认为被修复的类不包含在dex文件里面,尽管实际上这些类的实现还是在dex文件里面的。
19592b2603370274dd3f35caeb6541f89922718b

3.3 Sophix资源修复原理解析
之前在Instant Run里面也提到了资源修复,因为资源修复下发的是全量的资源包,所以并不适合在线上的环境中应用。想要在线上环境做资源修复肯定会使用差量的资源包,下发更新过的资源,而Sophix也是这样做的。
fe3424550b7ee8aef3d2d3510bd417c681a1834e
之前提到所有资源的访问都是被AssetManager类所代理的,AssetManager里面会有一个AssetPath数组,其中会有两个成员,一个是指向系统框架资源,一个应用的指向资源。Sophix资源修复的思路就是在AssetPath数组里面多加一个资源,这样当在前两个资源路径中都找不到时候就可以从新的资源路径中寻找,可以通过这样的方式去修复资源替换的问题。当资源修改之后会下发一个资源修复的补丁包,同时把补丁包集成到端上之后会在AssetManager里面通过addAssetPath方法添加一个新的成员,进而实现资源修复。

而Sophix的资源修复会涉及到以下三种情况:
  • 新增资源导致原有资源id偏移:对比新旧代码前,将新包中所引用的未修改资源ID修正。
  • 引用内容修改的资源:对比新旧代码前,在新包中将所引用的原有资源ID置为更新后的ID。
  • 删除资源:无需修改。
19b04043c4463bf053dd6f6e66f75831205ebcba

四、热修复方案的总结和对比
c39778340cadad4f02cc446d59a332cd1602e628
最后来总结和对比各种修复方案的原理和特点:
  • Qxxx方案原理比较简单,通过代码插桩绕开预校验问题,此外通过dexElement插入的方式使得带补丁的dex文件优先加载。其优点在于实现比较简单,可以修复大部分类层面的问题。但是同时其问题也是比较突出的,第一点是不支持实时生效,第二点就是全量插桩的方式侵入性非常强,同时性能损耗也是非常大的。
  • Instant Run方案原理同样用到了代码插桩,而且其插桩比Qxxx方案更加复杂,它还会用到宿主的application做很多准备工作,这之后才会去执行业务代码,最后它还会去通过AssetManager重建做资源修复工作。优点是它能同时支持方法更新、类更新和资源更新,并且在方法更新的过程中还可以做到及时修复,不需要重启App。其问题在于还是使用了全量插桩,所以侵入性很强,同时对于性能的损耗也很大,由于在进行修复时需要下发全量资源包,所以开销非常大,同时也不适合在实际的生产环境中使用。
  • Andfix方案的原理是native方法的替换,这个方法很巧妙,并且实现比较简单,而且可以做到及时生效。但是它不支持类结构的改变,同时因为不同版本虚拟机的方法体结构不同,无法实现兼容性的处理,所以这种方案的兼容性也比较差。
  • Sophix方案使用了很多很巧妙的原理实现,首先它还是使用native方法替换,这种方法会比Andfix更加巧妙,它不需要知道方法结构体具体的成员变量,而直接使用整体的替换,只需要知道方法结构体的大小即可。它使用了很巧妙的方式,通过两个紧密排列的方法的地址差完成了方法替换即时生效的功能。Sophix使用全量dex替换去完成冷启动修复场景,而在资源修复的时候使用了差量资源包注入的方式,最大限度地降低了网络的开销,只需要很轻量地把差量资源包下发就可以了。其优点在于同时支持方法更新、类更新和资源更新,而且包括native方法的替换以及资源包的注入等很多实现非常巧妙和优雅,也非常轻量,并且属于非侵入式的修复。