## 携程无线离线包增量更新方案实践 文/赵辛贵 >为了提高页面加载速度和成功率,携程团队在开发 Hybrid 技术之初就采用了离线包的方式,并经过持续的迭代优化,已经形成一套稳定、高效的无线离线包更新方案,本文作者即分享了其团队在增量更新方面的实践。 携程旅行 App 中近半数业务页面使用 HTML5 Hybrid 和 React Native 技术开发,为了提高页面加载速度和成功率,我们在开发 Hybird 技术之初就采用了离线包方式,即将 R H5 Hybrid 或 RN 开发的业务代码打包到 App 中,直接通过应用商店分发到用户终端。如果有业务功能变更,就通过我们的无线发布系统,将新的业务离线包更新到 App 中,从而做到随时发布,动态更新。当然,如果都是全量发布,App 在启动时就需要下载更大的离线包,增加用户流量的同时加大了下载失败的概率,因此需要考虑好增量更新的方案。 ### 离线包增量更新方案 图1这张简图介绍了我们是如何设计离线包增量更新方案的。从客户端的角度,整个流程分为两个部分,即离线包下载列表获取和离线包文件下载。 图1    离线包更新系统 图1 离线包更新系统 现在,以一个新的业务模块上线为例,对整个流程进行说明。 - 创建业务模块:在离线包管理系统里新增业务模块,并配置生效的 App、版本和环境(开发/测试/生产); - 发布业务模块:在离线包发布系统,选择业务代码仓库分支,然后 Build,发布; - App 打包,获取最新基准包:在打包系统中,嵌入下载最新离线包功能的脚本,确保每次打包都能将最新的离线包打包到 App 中,并将每个包的版本信息,一并打包到 App 中; - App 启动,获取最新离线包列表: App 启动之后,发送本地 App 中离线包的版本号,以及 App 的 ID 到服务端,服务端返回最新离线包的下载路径; - App 根据离线包列表,下载离线包:
获取到离线包列表之后,在后台线程中按顺序逐个下载; - 离线包安装:下载完成,解压,合并,安装。 在此我们对离线包管理系统和离线包发布系统这两个系统的功能进行简单介绍。首先,离线包管理系统主要负责以下功能: - 离线包元数据信息管理:
元数据包括唯一包名、适用的平台、优先级、负责人以及业务频道描述; - 离线包对应的 App 关系维护
:所有的离线包,最终都要打包到 App 中。考虑到灵活性和扩展性,需要维护离线包和 App+ 环境的关系; - 离线包的启停用控制:离线包在某个 App 版本中不再使用后,可以修改相关配置。 “App+版本+环境”的最新离线包查询列表
:打包 App 时,需要将最新的离线包打包进去。这个时候,就需要离线包管理系统提供查询最新离线包列表的 API。
 再看离线包发布系统,主要包含以下功能: - 拉取选择仓库分支的代码,然后 Build; - 发布 Build 完成的包
:修改数据库中该离线包版本,修- 改离线包管理系统中最新包的版本; - 灰度发布、回滚、停用支持:灰度切分流量下发离线包,发现有问题及时回滚,可以随时停止发布,避免影响更多用户; - 发布数据查询与监控:发布效果监控,可以查看升级百分比,也支持特定用户对于某个发布的结果查询。 ### 工程实践中的问题和解决方案 上面介绍了离线包增量更新方案,但在实际工程实践中,我们还是遇到了诸多问题,接下来进行逐个分析。 #### 包依赖管理 携程旅行 App 有超过100个离线包,每个离线包都是一个独立的功能或业务模块,这么多的业务之间必定有相互依赖的问题。要严谨地解决该问题,需要引入类似 Node 的包管理机制,但这样的解决方案对我们来说太重。 因此,我们设计了一套简单的依赖管理规则,来解决这个问题: - 使用数字标识离线包的优先级,数字越小,优先级越高; - 优先级越高的包,先下载安装; - 优先级相同的离线包,下载顺序和发布顺序一致。 实际使用过程中,我们只定义了两个优先级——0和100。0为框架类,公共业务类的离线包,100为业务功能的离线包。业务依赖框架正常,极少有业务之间的强依赖,偶尔有的时候,业务之间协调好发布顺序即可。 #### 动态差分 为了让用户能够尽快下载离线包,我们需要尽可能地减小每个离线包的大小。这个时候,就需要采取差分算法,计算最新发布的包和原始打包到 App 中的基准包之间的差量,然后下发给 App。我们的方案是使用 BSDiff 进行差分: - 服务端拿最新包和打包到 App 里面的基准包计算差量,生成 Patch; - 客户端下载到该 Patch 文件后,和打包到 App 里面的原始文件 Merge,生成最新包。 看起来很完美的方案,并且业内大多做离线包的差分都是采取这种成熟的方案。但其实际效果并不完美,我们发现偶尔会出现300多 KB 大小的离线包在差分之后,生成的包有100多 KB。 经过反复测试,我们发现 ZIP 文件解压之后比较里面的变化文件,生成 Diff 文件,然后将 Diff 文件生成一个 ZIP 包,比直接 BSDiff 计算两个 ZIP 包生成的 Diff 会小很多。基于这个测试,我们对 bspatch 做了一些改进,如图2所示。 图2    BSDiff改进 图2 BSDiff 改进 从图2中可以看到,生成 Patch 包的时候,只 ZIP 进去变更过和新增的文件。同时对每个变化过的文件,生成了一个 Hash 文件,这样可以确保客户端将 Diff 文件 Patch 到原始文件之后,能验证文件是否完整。这一点非常重要,因为 bspatch 执行时,不会因文件内容合并不正确而返回 Patch 失败。 图3是某个版本中发布的4个差分包,传统 BSDiff 方案和我们的优化方案使用后,最终实际下载包的大小对比,可以看出优化效果非常明显。 图3   差分效果对比 图3 差分效果对比 另外,由于打包到不同 App 定制/渠道包里面的各个离线包版本不同,所以差分包需要动态生成。客户端获取到最新离线包列表之后,会先通到 CDN 下载,如果 CDN 没有,再回源到源站服务器,这个时候触发动态差分包生成,生成完成之后,再推到 CDN 上。 #### App 端离线包下载 此部分主要由以下机制进行保障: - 重试机制
 在网络状态不好时,离线包会有下载失败的情况。为了减少网络因素导致的失败,需要增加重试策略,比如最多下载三次,第一次失败,间隔 15s 重试,第二次失败,隔 30s 再试一次,三次失败则会终止继续下载。 - 签名校验 
文件下载完成之后,需要检查其是否被篡改过。因为离线包里都是代码,必须保证代码的正确性,建议在下发离线包列表时,下发该文件的签名,下载完成之后,校验签名是否正确。 - 定时轮询 
最初我们的离线包列表是在 App 启动之后,会获取一次,然后下载。当时经常会有反馈离线包下载不及时,不重启就下载不到最新版本。 为了解决这个问题,也考虑过使用服务器推送的方案,但是成本较高。因此简单为第一次离线包下载完成之后,每隔10分钟再去服务器查询一次,是否有最新离线包列表,如果有,继续下载。这样保证了发布之后,用户网络正常情况下,最多间隔10分钟左右,离线包就可以被更新。 #### 离线包的使用、安装和加载 离线包是打包到 App 中,发布到应用市场,用户下载安装的,因此在本地使用之前,需要先解压安装。从服务器端下载到的新离线包版本,也需要解压安装才能使用,图4即是进入某个离线包业务的流程。 图4    离线包安装流程 图4 离线包安装流程 以业务 A 为例,简单说明离线包的下载安装过程: - 业务 A 的离线包下载成功之后,开启子线程合并下发的离线包 Anew.7z 和 App 包中的原始包 Abase.7z,合并成功后保存在离线包的工作目录,例如 A 的工作目录为 A _ work,将成功合并的目录命名为 A _ bak,不可直接覆盖该离线包的工作目录 A _ work,因为 A 业务可能正在被使用; - 进入 A 业务,如果发现有合并成功的离线包文件 A _ bak,先使用该文件覆盖 A _ work, 覆盖成功之后,加载页面时,需要清空缓存,Reload 页面,加载失败回滚 A _ work 目录。 #### 发布控制 - 灰度发布:离线包发布直接到达用户终端,为了确认发布的功能对用户带来的影响线,需要先观察一部分用户行为和数据,这样发布系统就需提供灰度发布功能。我们采用的默认设定规则是10分钟10%,30分钟50% ,1小时后达到100%; - 发布回滚:发布的包如果有问题,可以回滚到先前版本的包; - 停止发布:发布的包如果没有生效,也没有副作用,为了尽可能地减少影响,可以直接停止这个包的发布。 #### 端到端监控 离线包的下载、安装是一个复杂的过程,并且都是在 App 运行后台进行,用户并无感知,为了能了解发布的状态和结果,需要完整的数据采集和监控。 - 对于生产环境:可以以一次下载为起点,安装完成为终点,当作一个事务。每次事务完成,都记录一条日志。这些日志上报后,后端就可以根据这些日志进行监控,实现对端到端的离线包更新效果监控或告警; - 对于测试环境:可以在测试包中保持完善的文件日志,记录离线包下载更新的每一步。另外,可以提供 Debug 工具,查看每个离线包业务的版本号,如图5。  图5   增量信息查看 图5 增量信息查看 ### 小结 通过持续的迭代优化,这样一套无线离线包更新方案,已经稳定地运行在携程旅行主 App 和集团其它子 App 中。从目前生产统计数据看,我们的离线包在 iOS 和 Android 平台分别达到99.8%和99.5%的下载安装成功率,希望这项方案可以对有类似业务的读者有所参考。