为什么选择Tinker作为热修复方案?

作者: liaowenhao | 来源:发表于2018-01-28 21:23 被阅读199次

    热修复这种 非官方支持非常规 开发方式,在采用前一定要权衡清楚其作用与代价。

    一. Java层热修复方案

    由于Native层的热修复方案在兼容性和稳定性上存在缺陷因而此处不讲, 在Java层的做法主要有两种

    1. 优先查找补丁Dex

    优先查找补丁Dex
    原理

    将修改过的类汇集成一个Dex,在加载类时让ClassLoader优先加载修改过的类.

    方案
    • 基于ClassLoader的双亲委托模型,给原ClassLoader设置parent ClassLoader,这样查找类时,会优先会使用parent ClassLoader去加载补丁类,其余类交给原ClassLoader.
    • 将补丁Dex路径插入到原ClassLoader的DexPathList的最前面,由于ClassLoader查找类时会顺序遍历Dex,所以补丁Dex会先于旧文件被查找到.

    2. 插桩

    插桩

    对每个方法插一段逻辑,此逻辑判断方法是否被打补丁,是则执行新逻辑.
    当修改完补丁文件后,会通过Hook技术将补丁类设置到该旧类中,以便执行新逻辑.

    • 插桩
      以静态的方式修改第三方的代码,也就是从编译阶段,对源代码(中间代码)进行编译,而后重新打包,是静态的篡改; Gradle的Transform API技术可用于插桩.

    • Hook
      不需要在编译阶段修改第三方的源码或中间代码,是在运行时通过反射的方式修改调用,是一种动态的篡改.

    3. 应用

    InstantRun及美团Robus使用第二种方案, 成功率很高, 但是修复能力受限,需要为了修复而专门写修复的逻辑.

    Tinker及QZone是通过让VM优先加载补丁Dex里的类来使补丁生效,即Java派里的第一种方案.

    二. Tinker方案

    框架设计
    1. 补丁合成; 这些都在单独的 patch 进程工作,这里包括 dex,so 还有资源,主要完成补丁包的合成以及升级;
    2. 补丁加载; 通过反射系统加载合成好的 dex,so 与资源;
    3. 监控回调; 在合成与加载过程中,出现问题及时回调;
    4. 版本管理; Tinker 支持补丁升级,甚至是多个补丁不停的切换。这里我们需要保证所有进程版本的一致性;
    5. 安全校验; 无论在补丁合成还是加载,我们都需要有必要的安全校验。
    优点
    • 接入广
      github上的start数过万, 截止17年5月, 在应用宝Top 1000的应用中,有60多个应用已经使用了Tinker, 使用第三方平台接入Tinker并持续使用的应用也超过1000个。

    • 应用场景多
      支持类/so/资源的修复,可支持用户调试,版本升级,发布需求,Abtest 等场景.

    • 第三方支持平台多
      TinkerPatch, Bugly,Tinker-Manager,tinker-dex-dump等方便对Tinker进行使用及调试.

    • 厂商支持
      收益于微信的产品影响力,与HOVM厂商在Tinker兼容性等问题上建立紧密联系.

    • 加固支持
      联合乐加固/360/爱加密等加固厂商,协商了支持热修复的加固方案.

    总的来说,Tinker的修复能力及影响力都比较大,接入有保障.

    缺点
    • 占用Rom体积;大约是修改Dex数量的1.5倍(dexopt与dex压缩成jar)的大小。
    • 需要额外的合成过程;虽然单独放在一个进程上处理,但是合成时间的长短与内存消耗也会影响最终的成功率。
    • 合成成功率略小于美团Robust和QQ空间热修复方案,主要原因在于空间不足以及后台进程被杀。Tinker会尝试使用重试的方式,官方也一直在降低合成的耗时与内存,从而提升成功率。

    三. 补丁生成

    1. 差量更新

    在编译时通过新旧两个Dex生成差异path.dex. 在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新Dex。

    为了补丁包尽量的小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。它的粒度是Dex格式的每一项,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/QZone的粒度为class。

    2. BsDiff算法

    差量更新,尽可能多的利用old文件中已有的内容,尽可能少的加入新的内容来构建new文件. 算法过程:

    a. 对old文件中所有子字符串形成一个字典;
    b. 对比old文件和new文件,产生diff string和extra string;
    c. 将diff string 和extra string 以及相应的控制字用zip压缩成一个patch包。
    d. old包接收,解压,并根据patch信息还原new文件.

    它与格式无关,但对Dex效果不是特别好,而且生成产物大小非常不稳定。
    当前微信对于so与部分资源,依然使用bsdiff算法;

    3. DexDiff算法

    通过深入Dex格式,实现一套生成产物小,内存占用少以及支持增删改的算法。

    DexFile格式

    DexFile格式

    头文件概括的描述了整个 dex 文件的分布,包括每一个索引区的大小跟偏移。

    索引区的ids 是 identifiers 的缩写,表示每个数据的标识,索引区主要是指向数据区的偏移。

    数据区主要存储文件字节码及索引区ids指向的数据.

    如何做到补丁很小?

    1. 对于索引区指向的内容,采用二路归并算法,将新旧两个dex内容索引排序,针对需要调整index的数据,标记需 要进行的op_replace/op_delete/op_add三种操作及对应的新旧索引号. 然后在补丁合成时对基类dex包进行相应操作.

    2. 对于数据区的Code Section,难点在于如果排除由于String等索引变动等导致的代码"看上去"变化了,但是实际上代码没变化.比如补丁中新增了一个字符串,导致其余字符串的索引号发生变动,但是这部分的代码并没有改变.

      解决方案: 第一项已经将新旧String_ids的索引映射表存储下来,在解析代码指令的时候,先将旧String索引替换成新String索引,然后再比对代码的字节码指令.

    如何做到内存较少?

    这是因为DexDiff算法是每一个操作的处理,它无需一次性读入所有的数据。

    四. 补丁加载

    1. ClassLoader

    Android中有两种ClassLoader用于加载dex文件,BootClassLoader、BaseDexClassLoader.

    BootClassLoader是所有ClassLoader的最终parent,但BootClassLoader包内可见,外部无法使用.

    BaseDexClassLoader是PathClassLoader和DexClassLoader的共同parent,会对jar, zip,apk,dex文件生成一个对应的dex文件, dex文件会在第一次启动时执行解压优化的过程,将生成的文件存到 /data/dalvik-cache(针对PathClassLoader)或者optimizedDirectory(针对DexClassLoader)目录.

    • DexClassLoader 可以加载 jar/apk/dex,可以从 SD 卡中加载未安装的 apk;

    • PathClassLoader 一般只能加载系统中已经安装过的 apk. 会在应用启动时创建,从 data/app/… 安装目录下加载 apk 文件。

    2. PathClassLoader与DexClassLoader的区别

    出于安全问题,Android不允许直接使用手机外部存储这类可拆卸的(mounted),不可执行(noexec)的存储媒介, 作为可执行文件(so 库或者 dex 包,也就是一种动态链接库)的运行目录. 所以虚拟机在运行程序时, 加载的可执行程序一定要存在到内存存储.

    因为DexClassLoader 可以指定自己的 optimizedDirectory,optimizedDirectory是用于缓存需要加载的文件的,位于dex文件原有路径,所以是内存存储路径. DexClassLoader可以将外部dex复制到内部路径的 optimizedDirectory,从而达到加载外部dex的效果;

    而PathClassLoader 没有optimizedDirectory,所以它只能加载内部的 dex,这些大都是存在系统中已经安装过的 apk 里面的。

    3. 委托双亲模型(Parent-Delegation Model)

    ClassLoader在加载一个类的实例会遵循委托双亲模型:

    1. 会先查询当前 ClassLoader 实例是否加载过此类,有就返回;
    2. 如果没有,按照由下往上的继承路线上查询 Parent 是否已经加载过此类,有就返回 Parent 加载过的类;
    3. 如果都没加载过,再依次由上往下尝试查找目标类,直到查找并加载成功.

    简单来说,就是由下往上遍历所有已加载过的dex文件,再由上往下查找目标类.
    而且如果程序不重新启动,加载过的类就不会重新加载。

    类加载过程

    ClassLoader的ClassTable用于记录已加载过的类.

    没加载过的类会调用findClass()方法, 遍历DexPathList对象的dexElements数组的每个DexFile,DexFile的defineClassNative()native方法尝试加载目标类.

    优点

    共享功能,一些 Framework 层级的类一旦被顶层的 ClassLoader 加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。

    隔离功能,不同继承路线上的 ClassLoader 加载的类肯定不是同一个类,这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。

    在Java中, 同一个 Class = 相同的 ClassName + PackageName + ClassLoader

    4. 补丁加载

    补丁合成

    补丁合成,简单来讲,就是合成补丁的逆过程, 除了TinkerApplication及com.tencent.tinker.loader.*类,其他类会生成在新的全量补丁dex中.这个过程可能比较耗费时间与内存,所以是单独放在一个后台进程:patch中。

    类加载

    Tinker的补丁加载主要逻辑是基于MultiDex.

    MultiDex在调用MultiDex.install()安装多dex时,将dex插入到ClassLoader的dexElements尾部,且考虑到不同API版本对dexElements的实现不尽相通,所以不同版本的安装过程略有不同.

    Tinker的主要原理是也是分版本修改dexElements,将新生成的全量dex插入到ClassLoader的dexElements前面,因此可以优先找到补丁类.

    资源加载

    资源采用的是全量替换,即完全使用新的资源包,加载方式与老版本InstantRun一致, 如果资源发生变化, 则反射替换AssetManager,将发生改变的资源路径添加进来. 然后根据改动情况,选择是热部署/温部署/冷部署使其生效.

    so加载

    加载补丁so包时,则遍历检查的结果列表libs,找到要加载的类,调用System.load方法进行加载。

    相关文章

      网友评论

        本文标题:为什么选择Tinker作为热修复方案?

        本文链接:https://www.haomeiwen.com/subject/mgazaxtx.html