热修复原理学习(1)热修复技术介绍

今天开始学习热修复的原理知识,学习方向是阿里团队编写的《深入探索Android热修复技术原理》,所以研究的热修复框架是Sophix

之前对热修复的知识做过了解,具体是这一篇:热修复原理学习

1.热修复的出现

日常开发中,我们可能遇到下面的情况:

  • 线上版本出现了严重的bug,如果才刚刚发版,或者下个版本已经规定了具体的计划发布,这个时候为了解决bug,需要fix+测试+在各个应用市场重新发布,这会消耗大量的人力物力,代价比较大
  • 版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的Bug就会一直影响不升级版本的用户
  • 有一个小而重要的功能,需要短时间内完成版本覆盖,比如节日活动

而热修复框架就应运而生,它可以解决上述这些问题,对线上版本进行修复。

它的热更新流程是这样的:
热修复原理学习(1)热修复技术介绍
可见,热修复的开发流程更加灵活,它有以下几大优势:

  • 无需重新发版
  • 用户无感知修复Bug,无需下载新的应用,代价更小
  • 修复Bug成功率高,把损失降到最低

2.基本概念

App的安装包是 .apk文件,它其实是 ZIP封装的,我们通过右键 apk文件->打开方式->zip文件打开,可以看到一个apk文件的目录大概是这样的:
热修复原理学习(1)热修复技术介绍
下面来介绍一下这些文件:

  • AndroidMnifest.xml
    这个比较熟悉,只是这个地方的文件是二进制的,而且它合并了所有子项目的AndroidManifest文件
  • META-INF
    签名相关的文件,它可以校验APK中所有文件的合法性,从而识别唯一的开发者,不让其他人随意反编译我的APK文件
  • classes.dex
    是Apk解压后文件中最重要的组成部分,它是Android项目所有Java代码最终编译后的形式。由于Android是基于ART的,所以运行的是对 .class格式进行压缩合成的 .dex格式文件
  • resources.arsc
    该文件包含了所有资源的ID,以及他们具体的ID值和类型信息。 平常开发中的 R.xx类型的ID,实际上都是由 32位数字组成的资源ID,他们有各种不同的类型,要么是字符串,要么是数字,要么是资源路径。而如果是资源路径,必然指代的是res文件中的资源文件。
    arsc文件只是ID信息,而实际资源内容(xml、图片信息)都是放在res目录下的。
    在程序运行中,会先从arsc文件中找到相应ID所对应的资源路径,然后再访问实际的res文件中路径所在的资源。
  • assets
    也是资源,只是这些资源是不带资源ID的原始文件,因此,可以直接指定路径来访问这些资源。与arsc文件中的资源ID是完全不同的。
  • lib
    lib下就是 JNI相关的so库了,它有 armeabi、x86等库,apk会根据所安装Android设备的CPU的实际架构来选择具体加载哪个so库
  • res
    存放所有资源文件的目录

了解了Android系统的APK文件格式后,我们再结合App的运行过程,看下如何从本质上实现热修复功能。

(1)关于AndroidManifest的修复
关于AndroidManifest出现的bug是无法修复的。因为他是由系统进行解析的,系统会直接获取安装包里唯一的AndroidManifest文件,在解析过程中不会访问补丁包的信息。
因此如果想要增加四大组件,通常来说不可以直接添加的。
因此我们需要在AndroidManifest里面加载新增组件时,通常的做法是预先在安装包的AndroidManifest埋入代理的组件,每次新增组件时,进行偷梁换柱,通过预埋的代理组件实现与系统进程间的通信,这也是 插件化Activity的做法。

(2)代码的修复
由于所有的Java代码最终都会编译为 classes.dex格式文件,因此任何的热修复方案,想要改变代码逻辑,都需要在补丁包里包含一个新逻辑的 dex文件,然后在程序运行的时候加载这个dex文件,并且改变执行流,从执行原有安装包里的classes.dex文件引导到新的dex文件中去。

(3)资源的修复
主要修改资源包的内容。正常情况下,资源包就是整个APK安装包,如果想要新增一个原有安装包里不存在的资源,就必须修改资源包的内容。
所以,就必须要想办法把原有的安装包替换为新资源包,或者把新的资源包插入程序的查找过程中。而有些资源,比如桌面图标、通知栏图标以及RemoteView之类的资源,是由系统直接解析安装包里的资源得到的,因此对于这类资源, 任何热修复方案都无法进行资源的替换和修复。

(4)so库的修复
so库的修复思路应该是最明确的。在Android系统中,所有so库都是由 System load进行加载的,因此只要找到办法在加载的时候优先加载补丁包的so库,而不是原有安装包的so库,就能够进行完整的低层代码替换了。

3. Sophix介绍

阿里系的热修复框架最初为 Dexposed,这个框架方案由于对DVM过于依赖,导致在Android5.0后,该框架作废。
后来阿里支付宝团队又研发了Andfix,还有之后基于 Andfix、对业务逻辑解耦的阿里百川Hotfix。他们可以兼容 DVM/ART,而且可以满足基本的代码修复。但是他们也有缺点:

  • Andfix是由局限性的,其低层固定结构的替换方案稳定性不是很好
  • 通过改造代码绕过限制来达到相同的修复目的,但是这种方式既不美观也不方便
  • 只提供了代码层的修复,资源层和so库层的修复还未能实现

业界除了阿里系的热修复框架,还有微信的Tinker,饿了么的 Amigo,美团的Robust等,不过他们各自都有自身的局限性,或者不够稳定,或者补丁过大,或者效率低下,或者使用起来过于烦琐。实际上用户体验并不太好。

而淘宝团队所研发的Sophix,在Android热修复的三大领域:代码、资源、so库,以及安全性和易用性都做的很好。

下面看下 Sophix的方案与其他方案的对比(同阿里系最新比对 ):
热修复原理学习(1)热修复技术介绍

Sophix的功能非常强大,除了不支持四大组件的增加(因为会让原有的代码变的臃肿,对App运行流程的侵入性很强)。所以使用也是会收费,没有开源,但设置了免费阈值。

Sophix的核心设计理念,就是非侵入式。
Sophix的打包过程不会侵入Apk的构建流程中,我们所需要的知识已经生成完毕的新旧APK,而至于APK是如何生成的则不需要关心,无论是AS打包出来的,还是Eclopse打包出来的,在生成补丁的过程中不会改变任何打包组件,也不插入任何AOP代码。

在Sophix的方案中,唯一需要的就是初始化和请求补丁两行代码,甚至连Application入口类都不需要做任何更改,这样就给开发者带来了最大的透明度和*度。

4.技术概览

4.1 代码修复

代码修复有两大主要方案,一种是阿里系的低层替换方案,另外一种是腾讯系的类加载方案。
这两类方案各有优缺点:

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

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

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

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

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

而阿里团队也重新对代码的底层替换方案原理进行了深入思考,从如何克服其固有限制和兼容性入手,以一种更加优雅的替换思路,实现了即时生效的代码热修复方案。
最终Sophix实现的是一种不修改底层具体结构的替换方式,这种不仅解决了兼容性问题,而且忽略了底层ArtMethod结构的差异,不需要兼容各个Android版本,代码量大大减少。
即时以后Android版本不断修改ArtMethod的结构,只要保证ArtMethod数结构体是以线性结构排列,就能直接使用与将来的版本。

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

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

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

而Sophix,采用的也是全量合成dex的技术,该方案会直接利用Android原有的类查找与合成机制,快速合成新的全量dex文件。
这样既不需要处理合成时方法数超过原有方法数的情况,也不会对dex的结构进行破坏性重构。
Sophix重新编排了包中dex文件的顺序,这样,在虚拟机查找类的时候,会优先找到 classes.dex中的类,然后才是 classes2.dex…也可以看做是dex文件级别的类插桩方案。这个方式十分巧妙,它对旧包与补丁包中的classes.dex顺序进行了打破与重组,最终使系统可以自然地识别到这个顺序,以实现类覆盖的目的。这将会大大减少合成补丁的开销。

(3)双剑合璧
Sophix的代码修复体系正是同时涵盖了这两种方案。两种方案的结合,可以实现优势互补,完全兼顾的作用。

这两种方案进行了重大的改进。在补丁生成阶段,补丁工具会根据实际代码的变动情况自动选择,针对小修改,在底层替换方案限制范围内的,就直接采用底层替换修复,这样可以做到代码修复即时生效。而对于代码修改超出底层替换限制的,会使用类加载替换,这样做虽然及时性没那么好,但是总归可以达到热修复的目的。

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

4.2 资源修复

目前市场上的很多资源热修复方案基本上都是参考了 Instant Run的实现。
实际上 Instant Run的推出正是推动这次热修复浪潮的主要原因。

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

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

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

Sophix的做法:
Sophix并没有直接参考 Instant Run技术,而是另辟蹊径,构造了一个 package id为 0x66的资源包,这个包里面只包含需要改变的资源项,然后直接在原有的 AssetManager中通过 addAssetPath()函数添加这个包就行了。
由于补丁包的 package id为0x66,不与目前已经加载的地址为 0x7f的包冲突,因此直接加载到已有的 AssetManger中就可以直接使用了。
补丁包里面的资源,只包含原有包里面没有的新增资源,以及原有的内容发生了改变的资源。并且 sophix采用更加优雅的替换方式,直接在原有的 AssetManager对象进行解析和重构,这样所有原有对AssetManager对象的引用是没有发生改变的,所以就不需要向Instant Run那样进行繁琐的修改了。

Sophix的资源修复方案在性能上超过了Google官方的 Instant Run方案,整个资源替换的方案优势如下:

  • 不修改AssetManager的引用处,替换资源更快更完全(对比Instant Run以及所有的 模仿者的实现)
  • 不必下发完整包,补丁包中只包含变动的资源
  • 不需要在运行时合成完整包,不占用运行时计算和内存资源

4.3 so库修复

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

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