为什么要做热更新
当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。
重点是还会有原来的版本遗留,无论你怎么提示都有人放弃治疗,不愿意升级,强制不能使用体验又足够糟糕到让人不能启齿。
如果这是一个影响公司收入或者体验影响极其不好的Bug,那完蛋了,可能公司老板会对整个技术团队的技术能力丧失信心,其对技术人员的伤害是致命的。
最后最致命的是:有时候仅仅是因为不小心写错了一行代码,就让所有的加班都付之东流,苦不苦,冤不冤,想想都苦。
还有一种剧情是研发总监把锅甩给测试团队,测试不过关,测试摊摊手说我也不是神啊,总会有漏网之鱼。
那能不能神不知鬼不觉在没有产生较大影响前把bug快速修复了呢?
热更新的行业情况
先来说说Android
并不是因为Android更有料就先说他,而是它的用户量级比Iphone大,我们写文章也是讲究大数据分析的不是。
Andoid端在15年热补丁就比较火,先后出现了Dexposed、AndFix,Qzone超级补丁,微信的Tinker, 大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust。
再来看看Iphone端
技术上要在 iOS 上做到原生动态化比 Android 更容易,iOS 开发语言 Objective-C 天生动态,运行时都能随意替换方法,运行时加载动态库又是项很老的技术,只要我把增量的代码和资源打包到一个 framework 里,动态下发运行时加载,修 bug,加功能都不在话下,性能完全无损,这件事就结束了。
但是呢。苹果把加载动态库的功能给封了,动态库必须跟随安装包一起签名才能被加载,无法通过别的途径签名后再下发。
于是有了 waxPatch 和 JSPatch 这样的方案,以及异军突起不局限于热修复Bug而能做主体功能发布的React Native 和 Weex,后面又有了吊口味的滴滴的DynamicCocoa方案和OCScript
Android 热更新方案
技术派系:
- Native,代表有阿里的Dexposed、AndFix与腾讯的内部方案KKFix;
- Java,代表有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix、饿了么的amigo以及美团的robust。
Native流派与Java流派都有着自己的优缺点,它们具体差异大家可参考下文。事实上从来都没有最好的方案,只有最适合自己的。
下面我们来一一简单看下各热更新的实现方案:
阿里的 DexPosed
阿里开源项目,基于Xposed的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。
不同的是,Xposed通过劫持 zygote(须root),而dexposed通过劫持 java method (而非楼上说的劫持class loader方法),将java method改变为native,并且将这个方法的实现链接到一个通用的Native Dispatch方法上
用处,最大的自然是hotpatch,用这种东西来热替换某个导致崩溃的方法。手淘还有做的一件事,就是用它作性能监控。这主要得益于无侵入式的方法调用Befor和After事件,能够让我们很好的记录和分析一个方法的调用时间。
开源项目promeG/XLog就是基于dexposed实现的方法调用logging。
优点:运行时、方法级、性能损耗少、学习成本低、本进程
缺点:不支持art、不支持Dalvik 3.0,所以注定它会逐步失声,再多的优点也是徒劳
Qzon的超级补丁 HotFix
该方案基于的是android dex分包方案的,关于dex分包方案本身更多是为了解决Android的64K方法调用限制问题,具体的原因是:
- DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个 short(2^16=65536) 类型来保存的,导致了方法 id 的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。
- Dexopt 使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃
尽管在新版本的 Android 系统中,DexOpt 修复了方法数65K的限制问题,并且扩大了 LinearAlloc 限制,但是这套技术机制保留了下来。
分包的方案简单来说就是在打包时将应用的代码分成多个 dex,使得主 dex 的方法数和所需的 LinearAlloc 不超过系统限制。在应用启动或运行过程中,首先是主 dex 启动运行后,再加载从 dex,这样就绕开了这两个限制。
如何拆分和如何加载可以查看Google官方的方案 MultiDex
Qzon的超级补丁方案玩的是什么招呢?
把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。
Patch.dex中的A.class会优先加载,后续的dex中的A.class就不会加载直接跳过,达到修复目的。
核心问题:
当两个调用关系的类不在同一个DEX时,就会产生异常报错。我们知道,在APK安装时,虚拟机先将classes.dex优化成odex文件后才会执行。在这个过程中,会进行类的verify操作,如果调用关系的类都在同一个DEX中的话就会被打上CLASS_ISPREVERIFIED的标志,然后才会写入odex文件。具体如何解决这个问题可以参见QQ空间终端开发团队发布的 安卓App热补丁动态修复技术介绍。
优点:
- 没有合成整包(和微信Tinker比起来),产物比较小,比较灵活
- 可以实现类替换,兼容性高
不足:
- 不支持即时生效,必须通过重启才能生效。
- 为了实现修复这个过程,必须在应用中加入两个dex!dalvikhack.dex中只有一个类,对性能影响不大,但是对于patch.dex来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。
- 在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。
微信的 Tinker
这个项目之初最大难点在于如何突破Qzone方案的性能问题,通过研究Instant Run的冷插拔与buck的exopackage给了我们灵感。它们的思想都是全量替换新的Dex
因为使用全新的dex,所以自然绕开了Art地址可能错乱的问题,在Dalvik模式下也不需要插桩,加载全新的合成dex即可。
焦点问题是合并的过程会不会有问题,会不会耗时或者效率低? 为此腾讯在DEX方面也花了很多时间研究内部的格式以及如何做Merge和进行校验工作,详细了解可以查看 大腾讯的第一个开源项目Tinker 这篇文章
优势:
- 合成整包,不用再构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行
- 性能提高。兼容性和稳定性比较高。
- 开发者透明,不需要对包进行额外处理。
不足:
- 与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。
- 需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。
- 合并时占用额外磁盘空间,对于多DEX的应用来说,如果修改了多个DEX文件,就需要下发多个patch.dex与对应的classes.dex进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。
阿里的 Andfix
为何唯独Andfix能够做到即时生效呢?
原因是这样的,在app运行到一半的时候,所有需要发生变更的Class已经被加载过了,在Android上是无法对一个Class进行卸载的。而腾讯系的方案都是让Classloader去加载新的类。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新的类。从而达到热修复的目的。
Andfix采用的方法是,在已经加载了的类中直接在native层替换掉原有方法,是在原来类的基础上进行修改的。
以Art为例,每一个Java方法在art中都对应着一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等等。通过env->FromReflectedMethod,可以由Method对象得到这个方法对应的ArtMethod的真正起始地址。然后就可以把它强转为ArtMethod指针,从而对其所有成员进行修改。
这很有 C/C++ 研发的味道,实际上Andfix的核心代码replaceMethod就是用cpp写的。
IOS的热更新方案
苹果把加载动态库的功能给封了,动态库必须跟随安装包一起签名才能被加载,无法通过别的途径签名后再下发。
Wax
最早要从 Wax 这个项目开始说,大家都知道 Objective-C 有着非常强大的动态特性。比如说:
- 运行时构造类和方法
- 运行时替换方法的实现
实际上这两个能力是非常恐怖的,像脚本语言那样,文本即代码,无须编译。
后来出现了一个叫做 Wax 的项目(这个项目目前由阿里巴巴维护),这个项目打出的口号是用 Lua 来写 iOS 原生应用,当然现实中没有人会这样干,因为写起来实在是太痛苦了。但是鉴于 iOS 应用审核比写 Wax 还痛苦,所以 Wax 成为了做 HotFix 的最佳选择。
这个项目的做法是通过加载 Lua 脚本,动态的生成 Objective-C 的方法,通常用来替换掉出了问题的那个,Lua 脚本是可以动态下发的,所以也就实现了修复线上 bug 的使命。
当然,Wax 用起来是极为痛苦的,尤其是和 Objective-C 的类型转换。
JSPatch
iOS 7 的时候 Apple 推出了 JavaScriptCore,这是一个非常有趣的框架,他是 JS 与原生交互的桥梁,让你在原生和 JS 之间穿梭自如,现在 iOS 平台各种动态技术大多都是基于此。
JSCore 推出不久之后,一个更优秀的项目诞生了:由 bang 写的 JSPatch。这个项目无疑从各种角度碾压了 Wax,并且 JS 也比 Lua 更为人熟知,所以也就迅速替代 Wax 成为了热修复的主流选择。
JSPatch 的接入成本非常低,对项目的影响也非常小,不需要引入额外的脚本解释器(因为已经有 JSCore 了),并且 JS 写起来真的比 Lua 要爽很多。
2017年3月8日,很多iOS开发者收到了警告邮件,声称其App违规使用动态方法,责令限时整改,Jspatch一直就被打入冷宫了。
这次警告事件无疑是对iOS平台Native动态化是一次严重打击,其影响甚至可能波及到Android平台,毕竟Google也是禁止加载远程代码的,并且执行更为严格,只是管不到中国的Android开发而已。
2018-6-9