写点什么

大前端时代下的热修复平台建设

  • 2019-11-27
  • 本文字数:7080 字

    阅读完需:约 23 分钟

大前端时代下的热修复平台建设

随着移动需求的增加、移动项目的拓展,如果移动端应用出现 Bug 不能及时得到修复,影响的不仅仅是用户体验,还会造成业务上的损失,因此,建立一套完整的热修复平台迫在眉睫。基于此,本文作者所在的搜狗商业应用研发团队构建了一套移动热修复服务中间件平台,本文从系统架构到主要流程对解决方案进行了详细的呈现,无论是 iOS、Android、RN、Flutter 都可以借助这一思想来开发自己的热修复平台。

写在前面

移动应用开发与服务端开发有很大不同,服务端应用如果出现问题,可以通过发布新版本修复,或立即回滚到上一个版本,用户能够立刻感知到这一变化;而移动端应用则不同,即使立刻发布新版本修复了问题,也无法保证所有人都能更新到这个版本,如果用户不升级移动端应用,问题依然得不到修复。


此外,众所周知,iOS 发布 App 需要经过 AppStore 的审核,审核的周期几天甚至一周,虽然近来时间有所缩短,但如果想要快速发布新版本依然避免不了审核;而 Android 发布 App 虽然没有审核的过程,但国内多样化的应用市场,依然影响着新版本的发布。


因此,针对移动端应用的实时更新是非常必要的需求。随着移动需求的增加、移动项目的扩展,建立一套完整的热修复平台日益迫切。

热修复概念

2015 年以来,移动开发领域对热修复技术的讨论和分享越来越多,同时也陆续出现了一些不同的解决方案。业内普遍共识是把不用重新发布新版本,不更新 App 自身安装包,在用户无感知的情况下,就可以对应用当前版本实现 bug 修复、部分功能修改的技术解决方案称为热修复 HotFix。


对比常规的开发流程而言,热修复的开发流程显得更加灵活方便,优势很多:


  • 无需重新发版,实时高效修复 bug;

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

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

实现方式

目前的移动项目中既有使用 iOS/Android 原生技术进行开发,也有使用 React Native/Flutter/Weex 等跨平台和 H5 Hybrid 技术进行开发的。H5 的特性使其不用关注热更新的问题,原生开发和跨平台开发虽然都能实现热更新/热修复,但技术实现手段不尽相同,因此热修复平台必须能够提供对上述不同技术方案的支持。热修复本质上是动态化、插件化技术的一种形式,可以理解为动态加载一个插件,在不同平台由于系统底层实现的不同,采用了不同的实现方式。

iOS 系统

用于 iOS 原生应用热修复的第三方技术方案主要有 JSPatch 和 waxPatch/LuaView 等。主要技术原理是用脚本语言编写补丁 patch,下发给客户端,在客户端本地通过 Objective-C Runtime 在运行时进行类名/方法名反射,替换相应的类和方法实现。

Android 系统

普遍的修复原理都基于 DEX 分包方案,使用了多 DEX 加载的原理,大致的过程就是:把 BUG 方法修复以后,放到一个单独的 DEX 里,插入到 dexElements 数组的最前面,让虚拟机去加载修复完后的方法。在功能上已经支持类、资源的替换和新增,功能非常强大。

跨平台

目前比较主流的跨平台方案就是 React Native、Weex 和 Flutter 了,RN 和 Weex 原理类似都是通过 JavaScript 语言反射成原生语言代码去执行,是使用 JS 脚本语言来编写的,也就是“即读即运行”。我们在“读”之前将之替换成新版本的脚本,运行时执行的便是新的逻辑了。脚本本质上和图片资源一样,都是可以进行热修复的,所以热修复的原理也比较简单,只要下发相应的 JS 补丁包就可以了。业内比较成熟的方案有微软的 CodePush。


Flutter 由于出现的比较晚,在 Flutter 1.2.1 中,Google 提供了 ResourceUpdater,用来做包的检查和下载解压,可以理解为官方支持的热修复。许多公司为了使用方便也研发了自己的热修复方案。

存在的问题

由于系统的差异性,不同平台有着不同的解决方案,它们的原理各有不同,适用场景各异,到底采用哪种方案,是开发者比较头疼的问题。如针对 iOS 平台的 JSPatch、滴滴 DynamicCocoa、阿里聚划算 LuaView 等,Android 平台的 QQ 空间补丁方案、阿里 AndFix 和 Sophix 以及微信 Tinker 等等,当然也可根据技术实现原理自研。


不管使用哪种热修复技术,我们都需要后台服务的支持,不然就无法实现补丁的分发。由于不同技术团队选用的技术方案也不一样,导致存在以下几个问题:


  • 针对不同客户端平台需要单独开发不同的补丁上传下载后台;

  • 如果更换热修复技术方案,后台也需要做调整;

  • 客户端接入逻辑复杂。客户端接入不同的第三方 SDK,需要进行代码适配;

  • 缺乏数据监控和统计。修复是否成功无法得到相应的数据反馈;

  • 如果管理多个 App,需手动管理版本、渠道等多种复杂工作,增加出错隐患。

解决方案

虽然客户端由于平台的差异性选择的热修复技术不同,但服务端相对而言整体流程和处理策略是统一的,因此,热修复需要一个后台服务来上传、管理和分发修复脚本;同时,也要提供针对不同客户端的 SDK,封装向平台请求脚本、传输解密、版本管理等功能。基于以上几点考虑,我们构建了一套移动热修复服务中间件平台,主要功能包括:


  • 对 iOS、Android 原生技术及 React Native 技术开发的移动应用提供热修复服务;

  • 提供不同平台的客户端 SDK;

  • 提供分发平台,方便复补丁的上传和维护;

  • 有补丁的版本控制功能,可以进行更新、回滚操作等;

  • 支持补丁文件全量下发和按条件下发;

  • 补丁分发时进行加密传输,保证安全性;

  • 支持数据统计。

系统架构

热修复服务平台

提供应用管理、应用版本和补丁的管理、修复补丁的上传和分发、补丁异常时的回滚、传输过程中的加密传输、自定义加密的 RSA 密钥、按条件下发等功能。

客户端 SDK

提供了向后端请求补丁、补丁的版本管理、传输后的解密等功能。

主要流程

热修复涉及的主要模块是热修复服务平台和客户端 SDK,核心流程如下图所示:



通常情况的热修复流程如下:


  1. 首先需要在热修复平台创建应用,设置版本号等信息。一般该操作和应用发布、消息推送等功能一起在移动开发平台统一管理;

  2. 添加一个补丁脚本,并选择所属应用名、系统和版本,设置下发条件规则等信息;

  3. App 启动同时 SDK 也启动,并发送查询请求给服务端,请求携带 App 标识、App 版本号等参数;

  4. 服务端根据收到的请求和条件下发规则判断下发哪个修复脚本。如果该 App 在该版本中存在修复脚本,则返回当前生效的脚本信息和地址;

  5. 客户端 SDK 根据接口返回数据判断进行下载、回滚或不进行任何操作;如果未返回任何信息,说明不存在修复脚本需要下发,此时客户端 SDK 不进行任何操作;

  6. 客户端 SDK 对下载后的补丁脚本进行 RSA 校验,并执行脚本;

  7. 补丁脚本执行成功或者失败等 log 信息上传至后台,以供数据分析使用。

应用管理

热修复是针对 App 的某个版本进行修复,因此发补丁前必须创建相应的应用和版本。应用创建后会分配一个随机的 appKey,客户端 SDK 与服务端交互时必须携带 appKey,用于标识应用。


补丁管理

补丁列表

补丁列表显示历史上发布的补丁信息,可以根据应用名称和版本号进行查询。



相关说明如下:


  • 添加新补丁:添加补丁的入口;

  • 应用名称:显示应用名称,来自于创建应用时的输入;

  • 平台:显示应用所属平台,即 iOS/Android/React Native;

  • 应用版本号:应用的版本号,来自于添加应用版本时的输入;

  • 补丁版本号:补丁的版本号,该应用在该版本内的顺序递增;

  • 补丁大小:补丁文件的大小;

  • 补丁描述:来自于添加补丁时的输入;

  • 补丁状态:补丁的生效/失效状态,同一应用同一版本内,只有一个生效状态的补丁;

  • 下发状态:补丁的下发状态,有全量下发和条件下发两种;

  • 更新事件:该补丁的最近一次操作事件;

  • 显示内容:显示补丁的内容,只能显示 iOS 补丁的内容;

  • 全量下发:将补丁的下发状态改为全量下发;

  • 条件下发:将补丁的下发状态改为条件下发。

添加补丁

在发布一个修复补丁时,要将其上传至分发平台,并选择所属应用名和版本。上传后,分发平台存储补丁并存储补丁的相关信息后,由于一个版本内可能存在多个修复补丁,因此新上传的补丁会标记为生效,其他的历史补丁标记为失效状态。


存储

服务端由多台机器构成,需要使用统一的文件存储,不能使用本地的文件系统。


方案一:使用 DFS


将补丁文件存储到 DFS 中,在 MySQL 中记录应用、版本,以及 DFS 唯一标识的关系,并提供下载接口,用于 SDK 请求。


方案二:使用 CDN


将补丁文件上传至 CDN,客户端 SDK 下载时直接访问 CDN 的链接。CDN 支持高并发,且访问速度有保障。


考虑到 CDN 有代码暴露的风险,倾向于选择使用 DFS。

补丁下发

一个 App 可能发布了多个版本,一个版本内又可能发布过多个修复补丁。在向 SDK 下发补丁时,应该下发哪个补丁呢?


下发时遵循如下约定:首先,补丁是基于某个 App 版本的,App 不能跨版本请求补丁;其次,App 一个版本内的多个补丁,只下发最新生效的一个。这是由于 iOS 和 Android 的热修复原理决定的。


iOS 的热修复是通过运行时用补丁中的 JavaScript 代码动态替换 Objective-C 代码实现的,这就造成无法用补丁将 App 的版本整体升级。因此,在发布下一个版本时必须把上一个版本的补丁中的 JS 代码在新版本中用 OC 再实现一遍。例如,当 App 处于版本 A 时某个方法 foo 出现了问题,在补丁 A1 中用 JS 对 foo 方法进行了修复,在发布下一个 App 的版本 B 时,B 中 foo 方法必须用 OC 重写一遍。Android 的热修复是基于基准包,用修复后的新包与基准包 diff 后生成的补丁,客户端再加载补丁实现修复,这也要求在发布下一个 App 版本的时候,必须含有补丁中的内容。


此外,如果在一个版本内下发多个补丁的话,比如版本 A 中,发现了一个 bug,发布了一个修复补丁 A1;之后,发现了另一个 bug,再发布一个补丁 A2。如果 A1 和 A2 的内容彼此无关,那么就要求客户端 SDK 要加载多个补丁文件,当补丁之间存在依赖关系时,更需要控制加载的顺序,这无疑增加了复杂度,而且无法回滚到特定版本;对于 Android 而言,由于加载的补丁是基于基准包 diff 后的包,也做不到加载多个补丁。因此,针对同一版本内有多个补丁的情况,只下发最新的一个生效补丁。



不同 App 版本不能跨版本下发补丁



同一个 App 版本内多个补丁时只下发最后生效的版本

回滚

如果所发布的补丁存在问题,这会造成客户端 APP 本身出现异常,甚至应用闪退、完全不可用。针对这种情况,有两种方案,第一是再发布一个新的补丁,补丁中包括修正了的正确代码。另一种情况是,有可能错误难以定位或修正时间太长,根本来不及发布新补丁,那么必须及时将错误补丁回滚。


按照上一节中的下发策略,服务端只会下发当前生效的补丁,因此服务端在回滚的时候只需要简单地将目标补丁标记为生效即可。


传输安全

由于下发的补丁会改变客户端应用的行为,如果被人攻击替换代码,会造成很大危害,因此必须考虑传输过程中的安全性。


针对这一问题,设计了如下 3 个解决方案:


方案一:对称加密


若要让补丁在传输的过程中不会轻易被中间人截获替换,很容易想到的方式就是对补丁进行加密,可以用 zip 的加密压缩,也可以用 AES 等加密算法。


优点:实现非常简单。


缺点:是安全性低,容易被破解。因为密钥是要保存在客户端的,只要客户端被人拿去反编译,把密码字段找出来,就完成破解了。


方案二:HTTPS


第二个方案是直接使用 HTTPS 传输。


优点:安全性高,只要使用正确,证书在服务端未泄露,就不会被破解。


缺点:部署麻烦,需要服务器支持 HTTPS,门槛较高。另外客户端需要做好 HTTPS 的证书验证(有些使用者可能会漏掉这个验证,导致安全性大降)。如果服务器本来就支持 HTTPS,使用这种方案也是一种不错的选择。


方案三:RSA 校验



这种方式属于数字签名,用了跟 HTTPS 一样的非对称加密,只是简化了,把非对称加密只用于校验文件,而不解决传输过程中数据内容泄露的问题,而我们的目的只是防止传输过程中数据被篡改,对于数据内容泄露并不是太在意。整个校验过程如下:


  • 服务端计算出补丁文件的 MD5 值,作为这个文件的数字签名;

  • 服务端通过私钥加密第 1 步算出的 MD5 值,得到一个加密后的 MD5 值;

  • 把补丁文件和加密后的 MD5 值一起下发给客户端;

  • 客户端拿到加密后的 MD5 值,通过保存在客户端的公钥解密;

  • 客户端计算补丁文件的 MD5 值;

  • 对比第 4/5 步的两个 MD5 值(分别是客户端和服务端计算出来的 MD5 值),若相等则通过校验。


只要通过校验,就能确保补丁在传输的过程中没有被篡改,因为第三方若要篡改补丁文件,必须计算出新的补丁文件 MD5 并用私钥加密,客户端公钥才能解密出这个 MD5 值,而在服务端未泄露的情况下第三方是拿不到私钥的。


优点:非对称加密能够有效解决传输中被篡改的问题。


缺点:数据内容可能会泄露,其实在传输过程中不泄露,保存在本地同样会泄露,若对此在意,可以对补丁文件再加一层简单的对称加密。

自定义 RSA 密钥

分发平台使用一套默认的公钥/私钥进行补丁传输过程中的加密/解密,如果对安全性要求更高,可以在上传补丁时设置自定义的 RSA 私钥。

条件下发

很多时候在发布一个补丁时,需要在小范围内进行验证,比如特定某个 iOS 版本或者特定某个用户;在验证通过后再进行全网用户的下发。这时可以用到条件下发。


分发平台在发布补丁时可以选择使用条件下发,除上传补丁外,还可以填写条件语句,只有满足条件的设备才会执行修复补丁。条件语句由 key/value/运算符组成。条件语句的规则与代码中的条件表达式一致,支持“==、!=、>、<、>=、<=、&&、||”等运算符。如:


iOS>9.0 或者 userId == 200758 && role == 1


当补丁的下发状态处于条件下发,且条件语句与 SDK 上报参数中的条件一致时,才会将补丁发送给该 SDK。


计算条件表达式时,如果通过字符串解析和替换的处理等方式,开发繁琐且实现时不够优雅。可以使用 EL 表达式引擎解决这一问题。常见的表达式引擎有 Apache Commons 中的 JEXL(Java Expression Language)、fast-fel 等,甚至 Java 1.6 后自带的 JavaScript 脚本引擎也可以完成这个工作。在综合考量性能和易用性后选择了 JEXL 表达式引擎(测试样例见附录 1)。JEXL 除了支持基本的算术表达式外,也支持在表达式中访问对象的属性、访问数组和集合、调用 Java 方法等特性,对于表达式的使用有很强的扩展性。


下面是一个 JEXL 的例子:


数据统计

提供分日、分 App、分版本的补丁分发数据统计功能。

SDK 设计

iOS/Android 和各种跨平台方案只需实现接口的查询和 patch 包的下载即可,再根据所采用的热修复库实现对应平台的热修复功能。

查询接口

下载接口


以 iOS 为例:


客户端 SDK 启动时会发请求询问服务端,根据服务端返回数据进行相应处理。客户端 SDK 会保存下载到的修复脚本,避免重复下载造成的流量损失。具体流程如下:



  1. App 启动同时 SDK 也启动,并发送查询请求给服务端,请求携带 App 标识、App 版本号等参数;

  2. 服务端根据收到的请求判断下发哪个修复脚本。如果该 App 在该版本中存在修复脚本,则返回当前生效的脚本信息和地址。客户端 SDK 根据接口返回数据判断进行下载、回滚或不进行任何操作;

  3. 如果未返回任何信息,说明不存在修复脚本需要下发,此时客户端 SDK 不进行任何操作;

  4. 如果返回的脚本信息中,脚本的版本号等于本地生效脚本的版本,说明客户端保存的脚本已是最新的,SDK 直接执行本地保存的脚本;

  5. 如果返回的脚本信息中,脚本的版本号不等于本地生效脚本的版本,说明服务端有新的修复脚本发布,或发生了回滚操作,客户端 SDK 判断本地是否存在该版本的脚本,存在时直接执行本地脚本;不存在时发起下载请求获取脚本,并在本地缓存,然后执行。

争议

2017 年 3 月,众多 iOS 开发者收到苹果警告邮件,称其 App 违规使用动态方法,责令限时整改。这封邮件引起了开发者的恐慌,最后发现问题集中出现在两个热更新工具 Rollout 和 JSPatch 上。由于 JSPatch 在 iOS 业内的高覆盖率,这个事件影响几乎波及到了国内所有在 AppStore 上线的 App。


这次警告事件无疑是对 iOS 平台 Native 动态化是一次严重打击,其影响甚至可能波及到 Android 平台,毕竟 Google 也是禁止加载远程代码的,并且执行更为严格,只是管不到中国的 Android 开发而已。


在安卓平台,虽然谷歌没有能力像苹果一样干涉国内的开发,但插件化技术从另一方面遭遇了困境。这一困境就是安卓新版本以及国内各种魔改 ROM 对于底层的改动。安卓插件化技术依赖部分底层方法以及私有 API,而这些在新版本里是很有可能改动的,一旦修改了,插件化就会失效甚至出错。国内各大手机厂商的系统也喜欢对底层进行修改,它们的修改甚至都不会公开告知,因此兼容问题是插件化技术遇到的最大挑战。


2018 年发布的 Android 9.0,甚至要求开发者不得使用私有 API,少了这些 API,安卓开发被重新关回笼子里,还能玩的黑科技大大减少,无意之中竟然取得了和苹果警告类似的效果。


在苹果「热修复门」事件之后,iOS 动态化的工具都转入地下发展,关于这方面的研究和分享也急剧减少,甚至连整个 iOS 技术的分享也变少了。虽然最初通过代码混淆等可以骗过苹果审核,但是很快也被禁止了,滴滴声称的 DynamicCocoa 也迟迟没有开源,QQ 甚至开发了一个自己的中间语言 OCScript,还开发了一个自己的虚拟机 OCSVM 去执行它。


虽然使用企业证书发布的 App 不受应用市场的监管影响,但是整个行业对于热修复技术的研究和讨论也越来越少了,大家不得不寻找新的技术突破点,这种氛围也间接促进了跨平台技术的推广。尤其是 Flutter 发布之后,相较于以前的 RN 等跨平台技术拥有了更流畅的用户体验,许多大厂也开始积极使用。相信在不久的将来移动开发会形成原生开发与跨平台开发并驾齐驱的态势。

结束语

本文介绍了一种基于第三方或自研的热修复客户端技术,但又不强依赖特定服务的通用热修复中间件管理平台,可以实现安全、稳定、可靠的热修复补丁上传、分发、版本管理等功能,并提供完善的数据统计。在实际应用中,可以结合团队自身技术栈打造成更加通用的热修复管理平台。


2019-11-27 17:004606

评论

发布
暂无评论
发现更多内容

作业

飞雪

面向对象设计原则

elfkingw

极客大学架构师训练营

设计模式原则

张瑞浩

架构师训练营-每周学习总结2

水边

极客大学架构师训练营

架构师训练营第二周作业

小树林

学习笔记-第二周

薛定谔的🐴

极客大学架构师训练营

这 10 行比较字符串相等的代码给我整懵逼了,不信你也来看看

程序猿石头

性能优化 信息安全

每周 ARTS 第 33 期

落英坠露

ARTS 打卡计划

学习总结-编程本质与架构设计原则

飞雪

架构师训练营作业(二)

Glowry

极客大学架构师训练营

依赖倒置原则以及week2 作业

不在调上

极客大学架构师训练营

「架构师训练营」学习笔记:第 2 周

Amy

极客大学架构师训练营 作业

ARTS-week-3

youngitachi

ARTS 打卡计划 arts

week2 学习总结

不在调上

ARTS-WEEK2

Allen

架构师训练营-命题作业2

水边

极客大学架构师训练营

极客时间架构师训练营 - week2 - 作业2

jjn0703

极客大学架构师训练营

思维模型 - 组合式创新

石云升

思维模型 组合式创新 拆解组合

架构师训练营第2周作业

风吹

【省吾身】创新及其发生条件

luojiahu

创新 日常思考

ARTS-WEEK3

一周思进

ARTS 打卡计划

架构师训练营——Week2作业

Shawn

互金总结系列(2)-- 前后端分离

互金从业者X

「编程模型」C++资源引用

顿晓

c++ 智能指针 shared_ptr make_shared 资源引用

【荒于嬉】事务的特性及隔离级别

luojiahu

事务

设计模式

张瑞浩

如何更好的使用Gson

Jackey

Java Gson

认识依赖倒置原则(DIP)

极客大学架构师训练营 第二周作业

架构师训练营-学习笔记-第二周

心在飞

极客大学架构师训练营

以“血联网”缓解“血荒” “硬科技”赋能生物安全

CECBC

区块链技术 智慧血站 物联网化

极客时间架构师训练营 - week2 - 作业1

jjn0703

极客大学架构师训练营

大前端时代下的热修复平台建设_大前端_李晓清_InfoQ精选文章