之前写了一篇关于react-native-code-push的入门使用篇:微软的React Native热更新 - 使用篇,真的是很简单的使用,能热更新成功就行了。这一篇通过在项目中实战所遇到的问题,根据源码分析它的原理,来更深入的理解code-push。
这篇文章是在已经搭建好code-push环境(执行过
npm install --save react-native-code-push@latest
、
react-native link react-native-code-push
,并安装了code-push cli
且成功登陆)为基础下写的,没有使用CRNA来创建App。
部署与配置
部署(deployment)Test,Staging和Production
在真正的项目中,我们一般会分为开发版(Test),灰度版(Staging)和发布版(Production),在Test中我一般是用来跟踪code-push的执行,在Staging中其实是和Production是同样的代码,但是当要热修复线上版本时,先会发布热更新到Staging版,在Staging测过后再通过promoting推到Production中去。
大致步骤:
- 通过
code-push app add MyAppIOS ios react-native
来创建iOS端的App,或者通过code-push app add MyAppAndroid android react-native
创建Android端的App。 - 使用
code-push app ls
查看是否添加成功,默认会创建两个部署(deployment)环境:Staging和Production,可以通过code-push deployment ls MyAppIOS -k
来查看当前App所有的部署,-k
是用来查看部署的key
,这个key
是要方法原生项目中去的。 - 添加一个Test部署环境:
code-push deployment add MyAppIOS Test
,添加成功后,就可以通过code-push deployment ls MyAppIOS -k
来查看Test
部署环境下的key
了。
经常使用code-push --h来查看可以执行的操作
最后结果如下图所示:
在原生项目中动态部署
在上面有提过需要把部署的key
添加到原生项目中,这样在不同的运行环境下动态的使用对应的部署key
,例如在Staging
下使用Staging
的key
,在Relase
下使用Production
的key
,在Debug
下不使用热更新(如需在debug环境下测试code-push,可以在codePush.sync里的option参数中动态修改部署key
)。
在Android中动态部署key,并且在同一设备同时安装不同部署的Android包
有两种方式:
- 官方配置入口:https://github.com/Microsoft/react-native-code-push#multi-deployment-testing,通过添加
buildConfigFiled
使用对应的key
,同时在If you want to be able to install both debug and release builds simultaneously on the same device
中有提到在同一设备同时安装不同部署的Android包。 - 第二种方式是通过资源文件
R.string
来实现同样的效果,在app/src
中分别添加staging/res/values
和debug/res/values
两个文件夹,然后复制app/src/main/res/value/strings.xml
粘贴到刚新建的两个values
目录下,最后在代码中获取key
的方式为R.string.reactNativeCodePush_androidDeploymentKey
。
配置好后可以使用
./gradlew assembleStaging
来打包Staging下的apk,输出目录在./android/app/build/outputs/apk
下,没有在gradle中配置签名安装(adb install app-staging.apk
)会出现如下错误:Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES] React native,关于gradle的buildType的使用:http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Types
在iOS中动态部署key
官方配置入口:https://github.com/Microsoft/react-native-code-push#multi-deployment-testing。
在iPhone上同时安装相同App的不同部署包
让你的iOS应用在不同状态(debug, release)有不同的图标和标题
应用中经常遇到的技巧
Target binary version 和 Label
![]()
image.png
label
代表发布的更新版本,Target binary version
代表app的版本号。
1、使用patch打补丁,修改元数据属性。
使用场景:例如当你已经发布了一个更新,但是到有些情况下,比如--des
需要修改,--targetBinaryVersion
写错了,比如我的8.6.0
写成了8.6
,然后在我发布8.6.1
新版的时候就会拉取8.6
的版本更新,这个时候就可以code-push patch MyAppAndroid Production --label v4 --targetBinaryVersion 8.6.1
。
2、使用promote将Staging推到Production
使用场景:当你在指定的部署环境下测试更新时,例如Staging
,测试通过后,想把这个更新发布到正式生产环境Production
中,则可以使用code-push promote MyAppAndroid Staging Production
,这时可以修改一些元数据,例如--description
、--targetBinaryVersion
、--rollout
等。
3、使用rollback回滚
使用场景:当你发布的更新测试没通过时,可以回滚到之前的某个版本。code-push rollback MyAppAndroid Production
,当执行这个命令时它会在MyAppAndroid上的Production部署上再次发布一个release,这个release的代码和元属性与Production上倒数第二个版本一致。也可以通过可选参数--targetRelease
来指定rollback
到的版本,例如code-push rollback MyAppAndroid Production --targetRelase v2
,则会新建一个release,这个release的代码和元属性与v2
相同。
注意:这个回滚是主动回滚,与自动回滚不一样
4、使用debug查看是否使用了热更新版本
使用场景:当你想知道code-push的状态时,比如正在检查是否有更新包,正在下载,正在安装,当前加载的
bundle路径等,对于android可以使用code-push debug android
,对于iOS可以使用code-push debug ios
注意:debug ios必须在模拟器下才可以使用
5、使用deployment h查看更新状态
使用场景:在发布更新后,需要查看安装情况,可以通过code-push deployment h MyAppAndroid Production
来查看每一次更新的安装指标。
6、较难理解的发布参数
- Mandatory 代表是否强制性更新,这个属性只是简单的传递给客户端,具体要对这个属性如何处理是由客户端决定的,也就是说,如果在客户端使用
codePush.sync
时,updateDialog
为true
的情况下,如果-mandatory
为false
,则更新提示框会弹出两个按钮,一个是【确认更新】,一个是【取消更新】,但是在-mandatory
为true
的情况下就只有一个按钮【确认更新】用户没法拒绝安装这个更新。在updateDialog
为false
的情况下,-mandatory
就不起作用了,因为都会静默更新。注意:mandatory是服务器传给客户端的,它是一个“动态”属性,意思就是当你正在使用版本
v1
的更新,然后现在服务器上有v2
和v3
的更新可用,v2
的mandatory
为true
,v3
的mandatory
为false
,此时去check update
,服务器会返回v3
的更新属性给客户端,这时服务返回的v3
的mandatory
为true
,因为v3
在v2
之后发布的更新,它会被认为是包含v2
的所有更新信息的,竟然v2
有强制更新的需求,那跳过v2
直接更新到v3
的情况下,v3
也被要求强制更新。但是如果你当前是在使用v2
的更新包,check update
时服务器返回v3
的更新包属性,此时v3
的mandatory
为false
,因为对于v2
而言v3
不是强制要更新的。 - Disabled 默认是为
false
,顾名思义,这个参数的意思就是这个更新包是否让用户使用,如果为true,则不会让用户下载这个更新包,使用场景:- 当你想发布一个更新,但是却不想让这个更新立马生效,比如想对外公布一些信息后才让这个更新生效,这时候就可以使用
code-push promote MyAppAndroid Staging Production --disabled false
来发布更新到正式环境,在对外公布信息后,使用code-push patch MyAppAndroid Production --disabled true
来让用户可以使用这个更新。
- 当你想发布一个更新,但是却不想让这个更新立马生效,比如想对外公布一些信息后才让这个更新生效,这时候就可以使用
- Rollout 用来指定可以接收到这个更新的用户的百分比,取值范围为0-100,不指定时默认为100。如果你希望部分用户体验这个新的更新,然后在观察它的崩溃率和反馈后,在将这个更新发布给所有用户时,这个属性就非常有用。当部署中的最后一个更新包的
rollout
值小于100
,有三点要注意:- 不能发布新的更新包,除非最后一个更新包的
rollout
值被patch
为100
。 - 当
rollback
时,rollout
值会被置空(为100)。 - 当
promote
去其他部署时,rollout
会被置空(为100),可以重新指定--rollout
。
- 不能发布新的更新包,除非最后一个更新包的
7、理解安装指标(Install Metrics)数据
先来看下试用过程,现在有两个机子,分别为A和B
第一步:发了一个更新包,Install Metrics
中提示No install recorded
表示没有安装记录
第二步:A安装了这个更新包,并且现在正在使用这个更新包
第三步:给
v1
打了个
patch
,把
App Version
改为
1.0.0
,并且把元属性
Disabled
改为
true
第四步:A卸掉App,发现
Install Metrics
中的
Activite
为
0%
了(0 of 1),证明在
of
左边的数是会增降的,of右边的数是只会增不会降的,
of
左边的数代表当前
install
或者
receive
的总人数,当有用户卸载App,或者使用了更新的更新包时,这个数就会降低。因此它很好的解释了当前更新包有多少活跃用户,多少用户接收过这个安装包。
Install Metrics
中的
total
并没有改变,还是为
1
,代表有多少个用户
install
过这个更新包,这个数字只增不降,注意
total
与
active
的区别。
第五步:分别在A、B上安装这个App。发现图中数据和上图没有任何区别,那是因为
disabled
为
true
,因此不会接收这个更新包。
第六步:给v1打了个patch,把元属性
Disabled
改为
true
,让B
check update
,发现下图中
active
中
of
右边的数增加了
1
,代表多了一个用户
received
v1,但是
of
左边的数字为
0
,代表v1没有活跃用户,
total
的改变是多了
(1 pending)
,代表有一个用户
received
v1,但是还没有
install
(也就是
notifyApplicationReady
没被调用)
第七步:让A
check update
,发现
Active
没有任何改变,因为B以前就接收过v1。
total
中
pending
数为
2
了,代表有两个用户
received
v1。
第八步:让B
install
v1,
active
变为
50%
,可以看出
installed/received
为50%。
total
增加了
1
,代表v1多了一次
installed
,一共经历了
2
次
installed
,
(1 pending)
代表还有一个
received
。
第九步:让A
install
v1,
active
变为
100%
。
total
增加了
1
,代表v1多了一次
installed
,一共经历了
3
次
installed
,没有
pending
代表没有
received
。
第十步:发一个可以触发
rollback
的更新。
在
App.js
的构造函数中添加如下代码:
constructor() {
super(...arguments)
throw new Error('roll back')
}
然后发个更新出去:code-push release-react MyAppIOS ios -d Staging --dev false --des rollBackTest
此时code-push deployment h MyAppIOS Staging
为:
这时我们让A去
check update
,并且把
code-push debug ios
打开(注意debug必须使用模拟器)。发现v2的
total
直接从v1
total
中读下来,也就是说所有的v1用户都会
received
v2,
pending
为
1
代表A
recevied
v2,但没有
installed
。
这时,我们让A
installed
v2,发现A会闪退,然后再次进入App,发现
pending
没有了,但是
total
并没有增加,
active
也没有改变,
pending
的加到
rollbacks
去了。
此时
code-push debug ios
会打印
Update did not finish loading the last time, rolling back to a previous version.
第十一步:发布个修订版,修复v2产生的bug。然后让B安装。
哈哈,这个图看懂了吗,看懂了就代表了解它的意思了O(∩_∩)O哈哈~
第十二步:发布一个强制更新的更新包。
经过上面的测试,大致了解了Install metrics
中各个参数的意思,这里大概总结一下:
- Active 成功安装并运行当前release的用户的数量(当用户打开你的App就会运行这个release),这个数字会根据用户成功
installed
这个release或者离开这个release(installed了别的更新包,或者卸载了App),总之有它就知道当前release的活跃用户量 - Total 成功
installed
这个release的用户的数量,这个数量只会增不会减。 - Pending 当前这个release被下载的数量,但是还没有被
installed
,因此这一个数值会在release被下载时增长,在installed
时降低。这个指标主要是适配于没有为更新配置立马安装(mandatory)。如果你为更新配置了立马安装但是还是有pending,很有可能是你的App启动时没有调用notifyApplicationReady
。 - Rollbacks 这个数字代表在客户端自动回滚的数量,理想状态下,它应该为0,如果你发布了一个更新包,在
installing
中发生crash
,code-push将会把它回滚到之前的一个更新包中。
源码解读
js模块
code-push中Javascript API并不多,可以在JavaScript API查阅。
而快速接入的方法也就两种,一种是sync
,一种是root-level HOC
。现在来看HOC的源码:
//CodePush.js 456行
componentDidMount() {
if (options.checkFrequency === CodePush.CheckFrequency.MANUAL) {
//如果是手动检查更新,直接installed
CodePush.notifyAppReady();
} else {
...
//如果不是手动更新,则每次start app都会去sync
CodePush.sync(options, syncStatusCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback);
if (options.checkFrequency === CodePush.CheckFrequency.ON_APP_RESUME) {
//每次从后台恢复时sync
ReactNative.AppState.addEventListener("change", (newState) => {
newState === "active" && CodePush.sync(options, syncStatusCallback, downloadProgressCallback);
});
}
}
}
可以看出更新的代码是sync
:
//CodePush.js 344行
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
在checkForUpdate中会去拿App的版本号,部署key和当前更新包的hash值,确保客户端接受到服务器正确的更新包,有几种情况拿不到更新包,第一种是服务端没有更新包,第二种是服务端的更新包要求的版本号与当前App版本不符,第三种是服务端的更新包和App当前正在使用的更新包Hash值相同。
//CodePush.js 85行
//PackageMixins.remote(...)执行后返回一个对象包含两属性,分别是download和isPending。
//download是一个异步方法用来下载更新包,isPending初始值为false,表示没有installed。
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
当拿到更新包后会标志这个更新包是否回滚过。
//CodePush.js 362行
//如果有拿个更新包,但是这个更新包是安装失败的包,并且设置中配置忽略安装失败的包,则这个更新包会被忽略
const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
if (!remotePackage || updateShouldBeIgnored) {
if (updateShouldBeIgnored) {
log("An update is available, but it is being ignored due to having been previously rolled back.");
}
//会去原生端拿当前下载的更新包,如果这个更新包没有installed,就会提示安装这个更新包,如果已经installed就会提示已经是最新版本。
const currentPackage = await CodePush.getCurrentPackage();
if (currentPackage && currentPackage.isPending) {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
return CodePush.SyncStatus.UPDATE_INSTALLED;
} else {
syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
return CodePush.SyncStatus.UP_TO_DATE;
}
} else{
//如果设置中配置弹提示框,则根据mandatory弹出不同的提示框,根据用户的选择决定是否下载更新包。
//如果没有配置弹提示框,则直接下载更新包
...
}
下载的代码:
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
//使用之前提到的download方法来下载更新包。
const localPackage = await remotePackage.download(downloadProgressCallback);
//检查安装方式
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
//安装更新
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
});
return CodePush.SyncStatus.UPDATE_INSTALLED;
};
原生模块:
寻找jsbundle过程:getJSBundleFile
中改成了CodePush.getJSBundleFile()
,在这里面会判断是否有新下载的更新包,如果比本地新则加载这个更新包,否则加载本地包,
安装更新包过程:会把最新下好的包(pending package
)标记的pending updated
标记为false。
notifyAppReady:会将pending package
移除。
initializeUpdateAfterRestart:加载更新包的bundle时会执行这个方法。
- 在这里面会判断
pending package
是否存在如果不存在则代表没有要安装的更新包,方法结束。 - 如果
pending updated
为true
,如果为true则代表该更新包已经安装过,但是pending package
依然存在,代表是一个roll back的包,将package标记为failed,方法结束。 - 如果
pending updated
为false
,则代表这个更新包没有安装过,pending updated
会被标志为true
。
这样做的的用处是在如果下载并install这个更新包后却没有notifyAppReady
,则pending updated
会从false
变为true
,最后被roll back。如果调用了notifyAppReady
则不会被roll back。
Demo
地址:https://github.com/lyxia/CodePushExp
作者:lyxia_ios
链接:http://www.jianshu.com/p/6e96c6038d80
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。