"RN 热更新" 解决方案
本文主要分为以下几个部分:
- 实施新方案的背景
- 新热更新解决方案的具体描述
- 客户端热更新相关事件统计
- 需要后台配合所做的变更
- 如何发布补丁(第一步是什么,第二部是什么)
实施背景
现有的通过 https://github.com/google/diff-match-patch 进行补丁合并的方案仅适用于 jsbundle 这种纯文本的补丁更新,对于图片资源的需要进行全量更新而不能通过补丁来更新。同时在测试过程中,发现:
- 在 jsbundle 大小为 1.5M 的时候,差分出来的 patch 文件大概在 3M 左右。
- Android 在进行合并操作的时候,读 App 中的 jsbundle 大致需要1分钟,读本地的 .patch 文件大致需要 2 分钟,合并过程需要 1 分钟或数分钟,且在调试合并逻辑时发现各个部分的操作也都需要花费很多时间。
- 现有方案没有对合并文件进行 MD5 校验,因此无法确保合并后的文件就是预期的新版本 jsbundle。
- 现有方案没有对热更新相关的事件进行记录,无法准确统计热风新的成功率,覆盖范围等。
通过实施新方案后,得到的益处有:
- 图片加 bundle 一起压缩后的大小为 450 KB 左右,同样代码量原有方案大小在 1750 KB 左右,从而使得安装包小了 1.3 M 左右。
- 新增图标和修改一行代码后,生成的 patch 文件大小在 410 KB 左右,下载速度更快。
- 在测试环境测试时,补丁下载并进行合并生成新 jsbundle 文件的事件大致在 3 秒内,热更新速度更快。
- 发生合并操作后,支持对合并生成的新文件进行 MD5 校验,热更新稳定性更好。
- 合并错误和合并成功会进行事件统计,能有效监控线上版本热更新的成功率,并能够对当前用户的 RN 版本进行统计。
具体方案描述
打包脚本
对于客户端:
通过编写 node 脚本执行 RN 的打包命令,然后将 jsbundle 文件 (单个) 和图片文件 (多个) 压缩成一个 .zip 文件。
node 脚本会计算该 .zip 的 MD5,并且连同脚本配置的 "最低支持" 版本等信息生成一个 .json 文件来描述该 .zip 文件。
App 打安装包时会包含且仅需要包含该 .zip 文件和 .json 文件。
对于服务端:
node 脚本会根据脚本配置中的 "支持热更新的版本" 来生成 .patch 文件 (通过 bsdiff 这个差分工具来实现)
如果有生成 .patch 文件的话,也会生成一个对应的 .json 文件来描述该 .patch 文件。
同时,不管有木有 .patch 文件,脚本都会生成一个 android.json 或 ios.json 文件来描述当前最新的 bundle 版本、以及是否启用热更新等配置。
发布热更新时,需要将 .path (及对应的 .json 文件) 和 android.json/ios.json 这三个文件放置到服务器对应目录下即可。
App 正常渲染
第 1 次启动时:
App 在启动后(打开资产页面前),需要将安装包里面的 .zip 和 .json 文件复制到沙盒 (手机本地) 中,然后解压 .zip 得到 jsbundle 和对应的图片资源。
然后可根据情况启动预加载(以加快资产页面渲染速度),资产页面打开后会读取沙河中的 bundle 文件渲染页面 (图片会根据 bundle 的相对路径进行读取)。
第 2..N 次启动时:
如果本地有 bundle 文件,无需再进行复制解压等操作,直接预加载 bundle 然后进入资产页面即可。
App 热更新后渲染:
- 启动时,调用服务器接口查询热更新配置,如果有热更新:
- 下载对应当前安装包 bundle 版本和最新版本的 patch 文件到沙河中 (假设 App Bundle 版本是 0.0.2,最新版本是 0.0.3,则补丁文件名为 0.0.2_0.0.3.patch)
- 然后复制应用版本中的 .zip 文件到沙盒中,然后跟 .patch 文件合并生成新的 .zip 文件 ( old.zip + .patch = new.zip )
- 解压 new.zip,然后通知现有页面重新进行渲染 (new.zip 解压后的 jsbundle 文件路径跟之前是一样的)
- 如果没有热更新:
- 当做没事发生,谢谢。
脚本逻辑和使用
脚本代码及其打包后的产物都跟现有业务代码在同一个 Git 仓库中(可参考下面两张图):
- 脚本代码目录: ./scripts/rn-packer
- 脚本打包产物目录:./packer
执行 packer 脚本的命令:
|
---|
使用示例
假设我们之前已经打了个 0.0.2 版本的包,现在去修改了一行代码、修改了一张图片、增加了一张图片:
然后,我们到 ./scripts/rn-packer/configs.ts 中修改打包配置(新的版本是 0.0.3,并且需要打 0.0.2 热升级到 0.0.3 的补丁文件):
接着,再次执行 npm run packer 命令:
此时会发现我们在 app 目录的新 0.0.3 目录中得到了新的 .zip 文件,并且在 server 目录中的 0.0.2 中得到了该版本升级到新的 0.0.3 版本的补丁包。
在接着,将 server 目录下的生成的补丁文件和描述热更新信息的 "android.json / ios.json" 文件复制到服务端 (注意目录和打包脚本的目录是对应的):
以 Android 为例,放置服务端的两个 json 文件内容如下:
android.json
|
---|
0.0.2_0.0.3.json (仅作为 .patch 文件的描述,在热更新实际过程中不会用到)
|
---|
下面是热更新前后的效果图
- 打补丁前
- 打补丁后
热更新前后,Android 沙盒里面文件的变更
- 热更新前:
- 热更新后 (右边的箭头标错了,应该是 newBundleFile.zip 指向图片资源,同理左边箭头也错了):
Android 端的逻辑
启动时:复制安装包中的文件 zip 和 json 文件到本地,并解压。
进入到资产页面时,根据本地 jsbundle 文件渲染内容。
同时调用接口查询热更新信息(后面改为调用支持灰度更新的接口)。
调用支持灰度的热更新信息接口成功后,需要判断以下异常逻辑:
- 是否有热更新信息 (开启灰度后,不在白名单的用户不会返回)。
- 根据 open 字段表示判断是否启用热更新。
- 根据 latestBundleVersion 字段判断当前安装包中的 bundle 版本是不是最新的,是最新的话则不需要热更新。
- 根据 supportPatchBundleVersions 判断热更新是否支持当前安装包中的 bundle 版本,不支持的话则不需要热更新。
- 根据 minSupportAppVersion 判断当前 App 的版本是否支持热更新(因为后续可能会升级 RN SDK,不同的 RN SDK 不能使用同一个 bundle 文件)。
下载补丁后:
合并前后主要设计到沙盒中的三个目录:
- latest:始终作为存放最终渲染的 jsbundle 和图片资源文件的目录,执行合并操作前会被重命名为 backup。
- download:作为存放下载后的补丁,并进行补丁合并、MD5 检验的目录,逻辑都正确走完后会重命名成 latest。
- backup:作为 latest 的备份目录,如果合并操作失败的话,会重命名回 latest。
客户端事件统计
Android 和 iOS 开发人员在热更新过程中,需要通过 GrowingIO 统计这个过程中相关的自定义事件。
参考代码
// Android: track API调用示例二
GrowingIO gio = GrowingIO.getInstance();
JSONObject jsonObject = new JSONObject();
jsonObject.put("gender", "male");
jsonObject.put("age", "21");
gio.track("registerSuccess", jsonObject);
// iOS: track API调用示例二
[Growing track:@"registerSuccess" withVariable:@{@"gender":@"male", @"age":@"21"}];
自定义事件
事件名称:RN热更新
事件标识符:RnPatch
|
GIO 效果图如下:
后台需要配合实现的逻辑
- 提供一个类似 APP 升级的接口,支持根据白名单进行灰度发布。
- 假设开启灰度且当前请求该接口的用户在白名单内
- 如果是 Android 用户,读取 /fundmobile/packer/server/android/android.json 文件的内容并原封不动的返回到客户端。
- 如果是 iOS 用户,读取 /fundmobile/packer/server/ios/ios.json 文件的内容并原封不动的返回到客户端。
- 假设当前用户不在白名单内,直接返回空字符串 (就当做没有读取到对应平台的 json 文件内容) 给客户端。
接口设计
接口:https://www.puyifund.com/fundmobile/checkRnVersion.do
名称:查询 RN Bundle 版本信息
入参:无
出参:
| |||
示例:
|
---|
如何发布升级补丁
第一步
Android 和 iOS 开发人员分别提供补丁及其相关文件:
- Android 平台: 0.0.2_0.0.3.patch、0.0.2_0.0.3.json 和 android.json 这三个文件。
- iOS 平台: 0.0.2_0.0.3.patch、0.0.2_0.0.3.json 和 ios.json 这三个文件。
注意,上面的 patch 和 json 这两个文件,两个平台是不一样的。
另外,开发人员在提供上述文件时,注意比较是否跟生产包所携带的 .zip 是否对应。
如何比较?
- 在 46 环境进行实际热更新测试。
- 对比代码仓库 packer/app 目录下的 android-bundle.json / ios-bundle.json 和生产包的是否一致,
然后执行另外编写的校验脚本命令 'npm run packer-test' 进行模拟的补丁合并操作,日志会告诉你模拟是否成功。
第二步
配置人员 先开启灰度,然后将上述的 .patch、.json 文件放到服务器对应目录下:
Android 平台:
- /fundmobile/packer/server/android/0.0.2/0.0.2_0.0.3.patch
- /fundmobile/packer/server/android/0.0.2/0.0.2_0.0.3.json
- /fundmobile/packer/server/android/android.json
iOS 平台:
- /fundmobile/packer/server/ios/0.0.2/0.0.2_0.0.3.patch
- /fundmobile/packer/server/ios/0.0.2/0.0.2_0.0.3.json
- /fundmobile/packer/server/ios/ios.json
第三步
白名单用户测试热更新是否正常:
- 正常:关闭灰度,或在白名单用户热更新后观察一定时间在关闭。
- 不正常:开发人员分析原因
- 根据 android.json 和 ios.json 文件中的 「open」「latestBundleMD5」等字段判断是否开启热更新,支持的热更新版本,客户端版本。
- 根据 patch 文件的 json 文件中的 「version」、「platform」 和 「patchFileMD5 」等字段判断 patch 文件是否放错,或平台放置位置调换了。
- 其它。
附注
I. 白名单
文件位置:/webapps/fundmobile/WEB-INF/properties/grayuserlist.properties
配置示例(注意右边的值是 userId,不是手机号):
|
---|
上面的两个 userId 目前是通过 Debug 断点方式获取的,如下图:
II. 灰度开关
文件位置:/webapps/fundmobile/WEB-INF/properties/sysconfig.properties
配置示例(on 表示开启灰度,白名单用户才能获取到热更新信息;off 表示关闭灰度,所有用户都能获取到热更新信息,如果配置了的话 ):
|
---|
III. 几张示例图
IV. 测试用例 ( 重要 )
用例一:假设当前安装包的 bundle 版本是 0.0.2,在服务端发了 0.0.3 的补丁,能正常热升级。
用例二:在用例一基础上, 在服务端再发了个 0.0.4 的补丁,能正常热升级。
用例三:在用例一基础上, 在服务端再发了个 0.0.5 的补丁,也能正常热升级 ( 跨版本,但本质上还是从 0.0.2 → 升级到 0.0.5 )。
用例四:在用例一基础上,覆盖安装 bundle 版本为 0.0.3 的安装包,确保覆盖安装后的的 bundle 版本还是 0.0.3 。
用例五:在用例一基础上,覆盖安装 bundle 版本为 0.0.4 的安装包,确保覆盖安装后的的 bundle 版本变成了 0.0.4 。
用例六:在用例五基础上,在服务端发了个 0.0.6 的补丁,确保能正常热升级到 0.0.6 。