Fork me on GitHub

ReactNative之增量升级方案

起初萌芽

既然市面上已经有了大厂微软做后盾的CodePush,还有牛X的开源社区的Pushy,为什么还要自己做热更呢?那是有原因的,首先我们从CodePush热更新进行讲解。

CodePush

  • 其增量升级仅仅是针对图片资源的
  • 其升级服务器程序并不开源
  • 其升级的服务器在美国,国内访问很慢且不稳定

Pushy

  • 持续更新,后续用到会进行讲解

React Native的热更新就是下载新RN包替换老RN包,但是这样我们的问题就是每次的全量更新RN包会很大,一个影响app包的体积大小,而是浪费用户的带宽流量。那么我们需要考虑的就是增量更新,要达到增量更新的目的,就需要把老RN包和新RN包的差异性找出来,并且可以将这些差异与老RN包还原出新RN包。bsdiff和bspatch相关技术能实现目标。

何为增量?

一个完整的RN-App通常包含以下几个部分:

  • native代码部分(objc/java)
  • js代码部分,即RN业务代码、依赖的第三方
  • 图片资源部分

这里Native代码的更新,你就别多想了,无法在线升级,只能通过市场升级了。

能进行在线升级的只有js代码部分和图片资源部分,具体到代码部分就是bundle文件和assets文件夹。

针对js代码部分(即bundle文件)的增量指的是,代码改动有多少,增量补丁patch就有多少,哪些没有改动的代码部分是不在补丁范围内的。

针对图片部分(即assets文件夹)的增量指的是,升级补丁包中只包含新增的图片和有改动的图片。

那么在app端,下载升级补丁包,只需要和现在的版本进行合并,就能计算出最新版本的全量包。

总结流程如下:

  • 计算增量包:v10(新版本) - v1到v9(旧版本) = 增量包(v1-v10.zip、v2-v10.zip…)
  • app根据自己当前的版本(比如v6),下载对应的增量包(v6-v10.zip)
  • app中通过 v6(旧版本) + v6-v10.zip(增量包) = 新版本(v10),计算出新版本的全量包

当然这里只是简单的操作,实际情况要复杂的多,因为涉及到Native的代码,以及跨版本更新问题。后续进行详细讲解。

增量算法

assets增量算法,比较简单,就是比对,可以很容易的比较出新增文件和不同文件(使用md5)

bundle文件的增量算法,比较复杂,不过有google写的一个开源库,可以针对大字符串进行diff和path,并且支持java、objc、js等等语言,完全的满足了我们的需求。

只用到2个接口,具体请参考github上的文档

  1. 生成增量包时候:patch_make(text1, text2) => patches
  2. app生成全量包时候:patch_apply(patches, text1) => [text2, results]

google开源库地址:https://github.com/bystep15/google-diff-match-patch

关于bsdiff相关操作,另一篇博客bsdiff的使用中讲到.

增量更新的大致流程

1、服务器

使用bsdiff算法将老RN包和新RN包生成一个补丁patch文件,供客户端下载。

2、客户端

下载patch文件,使用bspatch算法将补丁patch文件和老RN包生成一个新的RN包

系统设计和各模块职责

bundle要求的app最小版本

因为js代码要依赖于native代码的,所以,jsbundle对app的版本是有要求的。

假如bundle依赖native的一个新的接口,这个接口在v3.0版本的app中才会发布,如果v2.0版本的app升级了这个bundle,那么必然会报错,严重的可能会导致app的崩溃。

安装包命名规范

1.命名规则:hot_老版本号_新版本号.patched

如:
第一个包,版本号为1,不需要补丁patch文件
第二个包,版本号为2,则与第一个包生成一个补丁文件,命名为hot_1_2.patched
第三个包,版本号为3,则与第一个包生成一个补丁文件,命名为hot_1_3.patched
             则与第二个包生成一个补丁文件,命名规则为hot_2_3.patched
第四个包,版本号为4,则与第一个包生成一个补丁文件,命名为hot_1_4.patched
             则与第二个包生成一个补丁,命名为hot_2_4.patched
             则与第三个包生成一个补丁,命名为hot_3_4.patched

2.服务器接口根据版本号,返回最新的补丁patch文件

如:服务器存在
hot_1_2.patchedhot_1_3.patchedhot_1_4.patchedhot_2_3.patchedhot_2_4.patchedhot_3_4.patched等这些补丁文件。

则请求结果如下:
当收到版本号为1的请求时,服务器返回hot_1_4.patched;
当收到版本号为2的请求时,服务器返回hot_2_4.patched;
当收到版本号为3的请求时,服务器返回hot_3_4.patched;

更为复杂的情况,由于我们时混合开发,当牵扯到RN与Native之间的交互发生变更时,我们就不能让热更新发挥作用了,否则会导致崩溃,这里需要做一下逻辑处理。

bundle仓库设计

存储全量bundle和增量patch

目录结构说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bundle   存放全量bundle和全量assets的目录,里面的文件基本上是使用react-native bundle命令生成的
0.1.0

0.2.0
android
略,同ios
ios
config.json 此版本的配置信息,包含要求app的最低版本等,手动配置
index.jsbundle 全量jsbundle文件,使用react-native bundle命令生成
assets 全量图片目录,使用react-native bundle命令生成
0.3.0


patch 存放增量补丁的目录,里面文件都是命令生成的,无需手动维护
0.1.0
第一版本无文件
0.2.0
android
略,同ios
ios
0.1.0-0.2.0.zip 增量包.zip
0.3.0
android
略,同ios
ios
0.1.0-0.3.0.zip 增量包.zip
0.2.0-0.3.0.zip 增量包.zip
update.json 所有的升级包信息
src 存放打包用的源码
lib 存放打包用依赖的第三方的源码
patch.js patch命令入口
update.json.js update.json命令入口

config.json示例

1
2
3
4
5
6
7
8
{
"v": "0.3.0", //版本
"min-v": "4.0.0", //此版本要求的最小app版本
"date": "2016-01-01", //打包日期
"des": [
"修复xxbug", "添加xx功能"
]
}

update.json示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[
{
"v": "0.1.0",
"min-v": "4.0.0",
"date": "2016-01-01",
"des": [
"修复xxbug",
"添加xx功能"
],
"iosBundleMd5": "11f82563f8fd3f22dccb80ad2297f7bc",
"androidBundleMd5": "11f82563f8fd3f22dccb80ad2297f7bc"
},
{
"v": "0.2.0",
"min-v": "4.0.0",
"date": "2016-01-01",
"des": [
"修复xxbug",
"添加xx功能"
],
"iosBundleMd5": "3ca2824b008132cee515c0ea29938ff2",
"androidBundleMd5": "3ca2824b008132cee515c0ea29938ff2"
},
{
"v": "0.3.0",
"min-v": "4.0.0",
"date": "2016-01-01",
"des": [
"修复xxbug",
"添加xx功能"
],
"iosBundleMd5": "dbb81d2383112abb50eb19970c486acd",
"androidBundleMd5": "dbb81d2383112abb50eb19970c486acd"
}
]

服务器端设计

参数说明

1
2
3
* bundleV: app中的bundle版本
* appV: app版本
* platform: app的平台

返回结果说明

1
2
3
4
5
6
7
8
9
* status : 本次请求后台是否发生了错误
* msg : 给用户看的中文提示信息,静默升级时候没什么用
* latestBundleV : 当前的最新bundle版本
* latestAppMinV : 最新bundle要求的app最低版本
* canUpdate : 能否升级,boolean
* canUpdateBundleV : 能升级的bundle版本
* canUpdateAppMinV : 能升级的bundle要求的app最低版本
* patchUrl : 补丁包相对地址
* platform : 平台:ios or android

场景一:

能升级,且能升级到最新版

1
2
3
4
5
6
7
8
9
10
11
{
status: 'success',
msg: '可以升级,bundle最新版为0.3.0',
latestBundleV: '0.3.0',
latestAppMinV: '4.0.0',
canUpdate: true,
canUpdateBundleV: '0.3.0',
canUpdateAppMinV: '4.0.0',
patchUrl: 'patch/0.3.0/ios/0.2.0-0.3.0.zip',
platform: 'ios'
}

场景二:

无需升级,已经是最新版本

1
2
3
4
5
6
{
status: 'success',
msg: '无需升级,已经是最新版本',
canUpdate: false,
platform: 'ios'
}

场景三:

能升级,能升级到当前appMinV的最新bundle版本,但不是最新的bundle,想升最新bundle,必须先升app

1
2
3
4
5
6
7
8
9
10
11
{
status: 'success',
msg: '可以升级,但app版本3.0.0太低,只能升到bundleV0.2.0,bundleV最新为0.3.0',
latestBundleV: '0.3.0',
latestAppMinV: '4.0.0',
canUpdate: true,
canUpdateBundleV: '0.2.0',
canUpdateAppMinV: '3.0.0',
patchUrl: 'patch/0.2.0/ios/0.1.0-0.2.0.zip',
platform: 'ios'
}

场景四:

不能升级,已经是当前appMinV的最新bundle,但不是最新的bundle,想升最新bundle,必须先升app

1
2
3
4
5
6
7
8
{
status: 'success',
msg: '不能升级,当前已经是app3.0.0的最新bundle了,但不是最新bundle0.3.0,想升级bundle到最新,请先升级app',
latestBundleV: '0.3.0',
latestAppMinV: '4.0.0',
canUpdate: false,
platform: 'ios'
}

Native端设计

客户端的主要工作就是发请求到服务器询问是否能升级,然后根据返回到信息,下载升级包,解压升级包,安装升级包

过程中要保证下载到文件是正确的(md5校验),要保证补丁安装之后的全量bundle文件是正确的(md5校验)

参考链接