为什么要做热更新
当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。
重点是还会有原来的版本遗留,无论你怎么提示都有人放弃治疗,不愿意升级,强制不能使用体验又足够糟糕到让人不能启齿。
如果这是一个影响公司收入或者体验影响极其不好的Bug,那完蛋了,可能公司老板会对整个技术团队的技术能力丧失信心,其对技术人员的伤害是致命的。
最后最致命的是:
有时候仅仅是因为不小心写错了一行代码,就让所有的加班都付之东流,苦不苦,冤不冤,想想都苦。
还有一种剧情是研发总监把锅甩给测试团队,测试不过关,测试摊摊手说我也不是神啊,总会有漏网之鱼.
那能不能神不知鬼不觉再没有产生较大影响前把bug快速修复了呢?
热更新的行业情况
先来说说Android
并不是因为Android更有料就先说他,而是它的用户量级比Iphone大,我们写文章也是讲究大数据分析的不是..
Andoid端在15年热补丁就比较火,先后出现了Dexposed、AndFix,Qzone超级补丁的类Nuwa方式,微信的Tinker, 大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust.
再来看看Iphone端
技术上要在 iOS 上做到原生动态化比 Android 更容易,iOS 开发语言 Objective-C 天生动态,运行时都能随意替换方法,运行时加载动态库又是项很老的技术,只要我把增量的代码和资源打包到一个 framework 里,动态下发运行时加载,修 bug,加功能都不在话下,性能完全无损,这件事就结束了。
但是呢。苹果把加载动态库的功能给封了,动态库必须跟随安装包一起签名才能被加载,无法通过别的途径签名后再下发。
于是有了 waxPatch 和 JSPatch 这样的方案,以及异军突起不局限于热修复Bug而能做主体功能发布的React Native 和 Weex,后面又有了吊口味的滴滴的DynamicCocoa方案和OCScript
热更新的技术原理
先来说JAVA
技术派系:
• 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
使用方法:
dexposed提供了3个使用方法:
beforeHookedMethod
afterHookedMethod
replaceHookedMethod
来看看使用方式,也极其简单.
优缺点:
来说说硬伤吧,不支持art,不支持art,不支持art。
不支持Dalvik 3.0.
所以注定它会逐步失声,再多的优点也是徒劳
插播一条硬广: 技术文章转发收录太多,此文出处 http://www.cnblogs.com/Creator/ 以及微信公众号: 互联网手艺人
Qzon的超级补丁方案
该方案基于的是android dex分包方案的,关于dex分包方案本身更多是为了解决Android的64K方法调用限制问题,具体的原因是:
• DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个 short 类型来保存的,导致了方法 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
http://developer.android.com/intl/zh-cn/tools/building/multidex.htm
Qzon的超级补丁方案玩的是什么招呢?
把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。
Patch.dex中的A.class会有优先加载,后续的dex中的A.class就不会加载直接跳过,达到修复目的。
核心问题:
当两个调用关系的类不在同一个DEX时,就会产生异常报错。我们知道,在APK安装时,虚拟机需要将classes.dex优化成odex文件,然后才会执行。在这个过程中,会进行类的verify操作,如果调用关系的类都在同一个DEX中的话就会被打上CLASS_ISPREVERIFIED的标志,然后才会写入odex文件。具体如何解决这个问题可以参见QQ空间终端开发团队QQ空间终端开发团队发布的” 安卓App热补丁动态修复技术介绍”
优缺点:
1.没有合成整包(和微信Tinker比起来),产物比较小,比较灵活
2.可以实现类替换,兼容性高。(某些三星手机不起作用)
不足:
1.不支持即时生效,必须通过重启才能生效。
2.为了实现修复这个过程,必须在应用中加入两个dex!dalvikhack.dex中只有一个类,对性能影响不大,但是对于patch.dex来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。
3.在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。
微信Tinker
根据微信内部人士介绍:微信tinker项目之初最大难点在于如何突破Qzone方案的性能问题,通过研究Instant Run的冷插拔与buck的exopackage给了我们灵感。它们的思想都是全量替换新的Dex
因为使用全新的dex,所以自然绕开了Art地址可能错乱的问题,在Dalvik模式下也不需要插桩,加载全新的合成dex即可。
焦点问题是合并的过程会不会有问题,会不会耗时或者效率低? 为此腾讯在DEX方面也花了很多时间研究内部的格式以及如何做Merge和进行校验工作,详细了解可以查看” 大腾讯的第一个开源项目「Tinker」”这篇文章
优势:
1. 合成整包,不用在构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行
2. 性能提高。兼容性和稳定性比较高。
3. 开发者透明,不需要对包进行额外处理。
不足:
1. 与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。
2. 需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。
3. 合并时占用额外磁盘空间,对于多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写的。
面临的挑战:
因为安卓各ROM乱象的原因,ArtMethod的结构可能会不一样, ArtMethod类包含些什么其实都是在编译阶段,在运行阶段可能不是这么回事,例如sizeof(ArtMethod)可能实际在各平台就完全不一样,但是我们在编译的时候就确定了值,直接操作容易改乱内存数据导致奔溃。
有什么好的方法来解决这个问题呢?
来看看奇技淫巧
由于f1和f2都是static方法,所以都属于direct ArtMethod Array。由于NativeStructsModel类中只存在这两个方法,因此它们肯定是相邻的。
那么我们就可以在JNI层取得它们地址的差值:
然后,就以这个methSize作为sizeof(ArtMethod),代入之前的代码。
问题就迎刃而解了。即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列就能完美兼容。
著:此方法最新方案并不在开源的方案中
最大的优势在于
1. BUG修复的即时性
2. 补丁包同样采用差量技术,生成的PATCH体积小
3. 对应用无侵入,几乎无性能损耗
不足:
1. 不支持新增字段,以及修改<init>方法,也不支持对资源的替换。
再来看看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 要爽很多。
3月8日,很多iOS开发者发了警告邮件,声称其App违规使用动态方法,责令限时整改,Jspatch一直就被打入冷宫了
这次警告事件无疑是对iOS平台Native动态化是一次严重打击,其影响甚至可能波及到Android平台,毕竟Google也是禁止加载远程代码的,并且执行更为严格,只是管不到中国的Android开发而已。
滴滴的DynamicCocoa
DynamicCocoa这种方案,绕了一个更大的道,从编译阶段入手,通过 clang 把 OC 代码编译成自己定制的 JS 格式,再动态下发去执行,做到原生开发,动态运行,主打动态添加功能,当然顺便把修 bug 也给支持了。手机 QQ 内部也有一个类似的方案,不过更进一步,他们通过 clang 把 OC 代码编译成自己定制的字节码动态下发,然后开发一个虚拟机去执行(惊呆了),同样实现了原生开发,动态运行,都是 NB 得很的方案。只要底层处理做得足够好,也是个成本低收益高的方案,不过目前都还没开源,在github上是一个只有两行README但是有1000+Star的神奇项目
DynamicCocoa与Jspatch 思路上都是实现 JS 和 OC 的互调:DynamicCocoa 的重点是动态化能力,优势在于完全不用写 JS 和更多的语法特性支持;对于 HotPatch 来说 JSPatch 是更加小巧、轻量的解决方案。
据说在滴滴 App 已经上线并使用了好几个版本,如滴滴小巴、专车接送机都有过 10k 级别的动态化模块上线。
20170612 苹果已经正式禁止热更新,给涉及到检测出来的开发者发了邮件,同时提供 App Store “自动更新的分阶段发布” 功能。
苹果是如何检测的呢,大概可以从给开发者的邮件看出来:
最后我们来看看苹果的灰度发布功能吧,对于一个花了将近3年时间做国内超大规模私有云的我来说,感受到了熟悉的味道(服务器端灰度发布也是一个套路)