V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
alibaichuan
V2EX  ›  问与答

Android 热修复技术选型——三大流派解析

  •  1
     
  •   alibaichuan · 2016-10-25 11:26:03 +08:00 · 1234 次点击
    这是一个创建于 2981 天前的主题,其中的信息可能已经有所发展或是发生改变。

    2015 年以来, Android 开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如 QQ 空间补丁方案、阿里 AndFix 以及微信 Tinker ,它们在原理各有不同,适用场景各异,到底采用哪种方案,是开发者比较头疼的问题。本文希望通过介绍 QQ 空间补丁、 Tinker 以及基于 AndFix 的阿里百川 HotFix 技术的原理分析和横向比较,帮助开发者更深入了解热修复方案。

    技术背景

    一、正常开发流程

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjIhgj32V0jMchNYeEe8ULiad9ISB9OnPYBEBI8v98stQpxtYNvNSm2fQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    从流程来看,传统的开发流程存在很多弊端:

    重新发布版本代价太大

    用户下载安装成本太高

    BUG 修复不及时,用户体验太差

    二、热修复开发流程

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gj5DAnvZrebACxWMtiaRFIhVOPYt3ReEDzBISPmGhnicoDMhmhML2K9rbA/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    而热修复的开发流程显得更加灵活,优势很多:

    无需重新发版,实时高效热修复

    用户无感知修复,无需下载新的应用,代价小

    修复成功率高,把损失降到最低

    业界热门的热修复技术

    热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的 AndFix 、 Dexposed ,腾讯 QQ 空间的超级补丁技术和微信的 Tinker 。最近阿里百川推出的 HotFix 热修复服务就基于 AndFix 技术,定位于线上紧急 BUG 的即时修复,所以 AndFix 技术这块我们重点分析阿里百川 HotFix 。下面,我们就分别介绍 QQ 空间超级热补丁技术和微信的 Tinker 以及阿里百川 HotFix 技术。

    一、 QQ 空间超级补丁技术

    超级补丁技术基于 DEX 分包方案,使用了多 DEX 加载的原理,大致的过程就是:把 BUG 方法修复以后,放到一个单独的 DEX 里,插入到 dexElements 数组的最前面,让虚拟机去加载修复完后的方法。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gj3fTYBUicZ7pyWI1YdqqLzDnwnBkaibDXjepWCMR6ibZNfyzojJECHnjag/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    当 patch.dex 中包含 Test.class 时就会优先加载,在后续的 DEX 中遇到 Test.class 的话就会直接返回而不去加载,这样就达到了修复的目的。

    但是有一个问题是,当两个调用关系的类不在同一个 DEX 时,就会产生异常报错。我们知道,在 APK 安装时,虚拟机需要将 classes.dex 优化成 odex 文件,然后才会执行。在这个过程中,会进行类的 verify 操作,如果调用关系的类都在同一个 DEX 中的话就会被打上 CLASS_ISPREVERIFIED 的标志,然后才会写入 odex 文件。

    所以,为了可以正常的进行打补丁修复,必须避免类被打上 CLASS_ISPREVERIFIED 标志,具体的做法就是单独放一个类在另外 DEX 中,让其他类调用。

    我们来逆向手机 QQ 空间 APK 看一下具体的实现:

    先进入程序入口 QZoneRealApplication ,在 attachBaseContext 中进行了两步操作:修复 CLASS_ISPREVERIFIED 标志导致的 unexpected DEX problem 异常、加载修复的 DEX 。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gjvib0AG7sRIaj09VcdiaNGEH0Rts1p4ncdqUMVrib2DHQYNIiapHdcUXNWQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    1. 修复 unexpectedDEX problem 异常

    先看代码:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjoqIaq7DD13PkIg5Lvmxh8KibBCkBt7kJgVBDyxmqWzoba9ic4OooNbKQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    可以看到,这里是要加载一个 libs 目录下的 dalvikhack.jar 。在项目的 assets/libs 找到该文件,解压得到 classes.dex 文件,逆向打开该 DEX 文件,

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjIHaia3ofwXRzF6s8H1YMucR6Vm6UeBoRgHolslcKxw9DbZ7HsCblmoA/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    通过不同的 DEX 加载进来,然后在每一个类的构造方法中引用其他 dex 中的唯一类 AnitLazyLoad ,避免类被打上 CLASS_ISPREVERIFIED 标志。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjzKcRcoo7iaWAJep7D7J67xdiakuddDtRyX2HRAMcmTTrUs2unh9gU8Pw/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    在无修复的情况下,将 DO_VERIFY_CLASSES 设置为 false ,提高性能。只有在需要修复的时候,才设置为 true 。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gjfjqvbfz5AibU3bgzqh3HQ4wQfNF4qGMWYSERvOFf1XmcwfLibv36moWw/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    至于如何加载进来,与接下来第二个步骤基本相同。

    1. 加载修复的 DEX

    从 loadPatchDex()方法进入,经过几次跳转,到达核心的代码段, SystemClassLoaderInjector.c()。由于进行了混淆和多次方法的跳转,于是将核心代码段做了如下整理:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjBMZBZibyOY3KvbrThibFXRLUkPO3aHDplQfSC8AKNibYc4xbHF9ElUPPA/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    修复的步骤为:

    1. 可以看出是通过获取到当前应用的 Classloader ,即为 BaseDexClassloader

    2. 通过反射获取到他的 DexPathList 属性对象 pathList

    3. 通过反射调用 pathList 的 dexElements 方法把 patch.dex 转化为 Element[]

    4. 两个 Element[]进行合并,把 patch.dex 放到最前面去

    5. 加载 Element[],达到修复目的

    整体的流程图如下:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjcMQWHeNibVtLdDSmr9hjIc7s7BX18Eibia3SGJbKtX9fqNazJ4iaNBYOBg/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    从流程图来看,可以很明显的找到这种方式的特点:

    优势:

    没有合成整包(和微信 Tinker 比起来),产物比较小,比较灵活

    可以实现类替换,兼容性高。(某些三星手机不起作用)

    不足:

    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 ,达到修复的目的。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjUIsQkrjos0ibhsA0dDflq4lic6FLWiaWgujX0k7Syvib8FGRuVCtia2A7Tw/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    我们来逆向微信 APK 看一下具体的实现:

    先找到应用入口 TinkerApplication ,在 onBaseContextAttached()调用了 loadTinker(),

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjxCM4FlzFa8QEMaJflrqZsSrawUJia4TwzA1tXsQtruMleCvkKgydICA/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    进入 TinkerLoader 的 tryLoad()方法中,

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjFVy1jTdpbNsbj3kyKAzq2VhHjpQewS2nXjnXPxicGuialQSshmX4L99g/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    从方法名可以预见,在 tryLoadPatchFilesInternal()中尝试加载本地的补丁,再经过跳转进入核心修复功能类 SystemClassLoaderAdder.class 中。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjyPLfmp4Y6NKiaSPkPortnhicphG6EkEicbwQthSCwyKt3gJPq7IxE2lTA/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    代码中可以看出,根据 Android 版本的不同,分别采取具体的修复操作,不过原理都是一样的。我们以 V19 为例,

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjFIv4tiaMtPM343kJzHzmDm7Cu8ySqSLicViaLObTdpQsFHqM8GQ1SjLvw/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    从代码中可以看到,通过反射操作得到 PathClassLoader 的 DexPatchList,反射调用 patchlist 的 makeDexElements()方法吧本地的 dex 文件直接替换到 Element[]数组中去,达到修复的目的。

    对于如何进行 patch.dex 与 classes.dex 的合并操作,这里微信开启了一个新的进程,开启新进程的服务 TinkerPatchService 进行合并。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjbG7W5YuhwXS9Kqhm1wMRwLvPDIOyg2KZZdibjfj2eeu57Kuviagn1G2w/640?wxfrom=5&wx_lazy=1

    整体的流程如下:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gjv0qcLcUlE6DYiayfWlzlRlWlABeiaKYsnibicMlVAJAwPq0M0xLY57YGyA/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    从流程图来看,同样可以很明显的找到这种方式的特点:

    优势:

    合成整包,不用在构造函数插入代码,防止 verify , verify 和 opt 在编译期间就已经完成,不会在运行期间进行

    性能提高。兼容性和稳定性比较高。

    开发者透明,不需要对包进行额外处理。

    不足:

    与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。

    需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。

    合并时占用额外磁盘空间,对于多 DEX 的应用来说,如果修改了多个 DEX 文件,就需要下发多个 patch.dex 与对应的 classes.dex 进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。

    三、阿里百川 HotFix

    阿里百川推出的热修复 HotFix 服务,相对于 QQ 空间超级补丁技术和微信 Tinker 来说,定位于紧急 bug 修复的场景下,能够最及时的修复 bug ,下拉补丁立即生效无需等待。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjD4LMUSUDtmsSejor3BUNOd8skhZWNBoK1EI4DJNNAcibKEuia9dDySlw/640?wxfrom=5&wx_lazy=1

    1 、 AndFix 实现原理

    AndFix 不同于 QQ 空间超级补丁技术和微信 Tinker 通过增加或替换整个 DEX 的方案,提供了一种运行时在 Native 修改 Filed 指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。

    原理图如下:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gj9EWlCpBIbkNwfLJupIWLhMHKiaZaoI8RUW7iaabXLV7DIBVCTEP2rraw/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    2 、 AndFix 实现过程

    对于实现方法的替换,需要在 Native 层操作,经过三个步骤:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjzrpicfibeMnH7cZTeibLjtJa2l2SN7Rozp4VdCVfibgRTyznw2QRjwulSw/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    接下来以 Dalvik 设备为例,来分析具体的实现过程:

    2.1 setup()

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gj4f24aD1rvC1593OECicb83Ch4kJmcGQRFT8CyjISXQjWRDvKxianicosg/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    对于 Dalvik 来说,遵循 JIT 即时编译机制,需要在运行时装载 libdvm.so 动态库,获取以下内部函数:

    1 ) dvmThreadSelf( ):查询当前的线程;

    2 ) dvmDecodeIndirectRef():根据当前线程获得 ClassObject 对象。

    2.2 setFieldFlag

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjStQxdse5OpAibKV2bV9m8zaicPyFNmSrULiaFjoW4Kl49UWiaRf5uvYWQQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    该操作的目的:让 private 、 protected 的方法和字段可被动态库看见并识别。原因在于动态库会忽略非 public 属性的字段和方法。

    2.3 replaceMethod

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjXZwOJVK2bzON9Hb9hbibrPO2obIaw1ibibicle4mIw0vBdUgxaGibSx7BTA/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    该步骤是方法替换的核心,替换的流程如下:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjFENrkibicyFAyeJribaia0mQ1DEUIIFz7ozaH6HaQaFtDMTW0bbTnfKQvw/640?wxfrom=5&wx_lazy=1

    AndFix 对 ART 设备同样支持,具体的过程与 Dalvik 相似,这里不再赘述。

    从技术原理,不难看出阿里百川 HotFix 的几个特点:

    最大的优势在于

    BUG 修复的即时性

    补丁包同样采用差量技术,生成的 PATCH 体积小

    对应用无侵入,几乎无性能损耗

    不足:

    不支持新增字段,以及修改<init>方法,也不支持对资源的替换。

    由于厂商的自定义 ROM ,对少数机型暂不支持。

    综合分析如下:

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6Gjq2SQY1r2H8PbjpcYiaQYezXcOb58gUeDmcOzjsIBDibftO1wtWZdQNPg/640?wx_fmt=png&wxfrom=5&wx_lazy=1

    热修复的坑和解

    我们可以看到, QQ 空间超级补丁技术和微信 Tinker 的修复原理都基于类加载,在功能上已经支持类、资源的替换和新增,功能非常强大。既然已经有了这么强大的热修复技术,为什么阿里百川还要推出自己的热修复方案 HotFix 呢?

    一、多 DEX 带来的性能问题和影响

    我们知道,多 DEX 方案用来解决应用方法数 65k 的问题,现在 Google 也官方支持了 MultiDex 的实现方案。但是,这实在是应用因方法数超出而作出的不得已的下策,但是超级补丁技术和 Tinker 作为一种热修复的方案,平生给应用增加了多个 DEX ,而多 DEX 技术最大的问题在于性能上的坑,因此基于这种方案的补丁技术影响应用的性能是无疑的。

    1. 启动加载时间过长

    我们可以看到,超级补丁技术和 Tinker 都选择在 Application 的 attachBaseContext()进行补丁 dex 的加载,即使这是加载 dex 的最佳时机,但是依然会带来很大的性能问题,首当其冲的就是启动时间太长。

    对于补丁 DEX 来说,应用启动时虚拟机会进行 dexopt 操作,将 patch.dex 文件转换成 odex 文件,这个过程非常耗时。而这个过程,又要求需要在主线程中,以同步的方式执行,否则无法成功进行修复。就 DEX 的加载时间,大概做了以下的时间测试。

    http://mmbiz.qpic.cn/mmbiz_png/ibnDKD3ktAoYuHH00jQEGUrbLiawkYC6GjL9lVicHUfgSt7UB6RmnyiaS64OiayOrOiak0mwcu3kzcCrGvn0zod8CEmA/640?wxfrom=5&wx_lazy=1

    随着 patch.dex 的增加,在不做任何优化的情况下,启动时间也直线增长。对于一个应用来说,这简直是灾难性的。

    1. 易造成应用的 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 插件化?

    我们经常提到热修复和插件化,这都是当下热门的新兴技术。在讲述之前,需要对这两个概念进行一下解释。

    插件化:一个程序划分为不同的部分,以插件的形式加载到应用中去,本质上它使用的技术还是热修复技术,只是加入了更多工程实践,让它支持大规模的代码更新以及资源和 SO 包的更新。

    热修复:当线上应用出现紧急 BUG ,为了避免重新发版,并且保证修复的及时性而进行的一项在线推送补丁的修复方案。

    显然,从概念上我们可以看到,插件化使用场景更多是功能,热修复使用常见在于修复。从这个层面来说,插件化必然功能更加强大,能做的事情也更多。 QQ 空间超级补丁技术和微信 Tinker 从类、资源的替换和更新上来看,与其说是热修复,不如说是插件化。

    当然,强大的功能也就增加了不稳定的因素。比如上文提到的增加启动时间,导致 ANR 、 Crash 的问题。

    QQ 空间超级补丁技术和微信 Tinker 提供了更加强大的功能,但是对应用的性能和稳定有较大的影响,就 BUG 修复的这个使用场景上还不够明确,并且显得过重。

    针对应用的性能损耗,我们可以举例做一个对比。

    某 APP 的启动载入时间为 3s 左右,本身就是基于多 DEX 模式的实现。

    分别接入三种热修复服务,根据腾讯提供超级补丁技术和 Tinker 的数据,那么会变成以下的场景:

    阿里百川 HotFix:启动时间几乎无增加,不增加运行期额外的磁盘消耗。

    QQ 空间超级补丁技术:如果应用有 700 个类,启动耗时增加超过 2.5s ,达到 5.5s 以上。

    微信 Tinker :假设应用有 5 个 DEX 文件,分别修改了这 5 个 DEX ,产生 5 个 patch.dex 文件,就要进行 5 次的 patch 合并动作,假设每个补丁 1M ,那么就要多占用 7.5M 的磁盘空间。

    显然对于修复紧急 BUG 这个场景,阿里百川 HotFix 的更为合适,它更加轻量,可以在不重启的情况下生效,且对性能几乎没有影响。微信 Tinker 、 QQ 空间超级补丁技术更多地把场景定位在发布小的新功能上,采用 ClassLoader 的模式,牺牲较高的性能代价去实现类、资源新增或替换的功能。阿里百川 HotFix 对应用本身做到无侵入,无性能损耗。

    总结

    QQ 空间超级补丁技术和微信 Tinker 支持新增类和资源的替换,在一些功能化的更新上更为强大,但对应用的性能和稳定会有的一定的影响;阿里百川 HotFix 虽然暂时不支持新增类和资源的替换,对新功能的发布也有所限制,但是作为一项定位为线上紧急 BUG 的热修复的服务来说,能够真正做到 BUG 即时修复用户无感知,同时保证对应用性能不产生不必要的损耗,在热修复方面不失为一个好的选择。

    阿里百川 HotFix 公测正在进行中,

    申请地址:

    http://baichuan.taobao.com/product/hotfix.htm

    2 条回复    2016-10-25 20:32:42 +08:00
    zijing07
        1
    zijing07  
       2016-10-25 13:48:00 +08:00
    好文!不过图都只剩链接,没有预览了。
    alibaichuan
        2
    alibaichuan  
    OP
       2016-10-25 20:32:42 +08:00
    @zijing07 谢谢!今天第一次与大家见面,以后不断优化啊!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2271 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 16:09 · PVG 00:09 · LAX 08:09 · JFK 11:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.