以前写了一篇关于react-native-code-push的入门使用篇:微软的React Native热更新 - 使用篇,真的是很简单的使用,能热更新成功就好了。这一篇经过在项目中实战所遇到的问题,根据源码分析它的原理,来更深刻的理解code-push。java
这篇文章是在已经搭建好code-push环境(执行过
npm install --save react-native-code-push@latest
、react-native link react-native-code-push
,并安装了code-push cli
且成功登录)为基础下写的,没有使用CRNA来建立App。react
在真正的项目中,咱们通常会分为开发版(Test),灰度版(Staging)和发布版(Production),在Test中我通常是用来跟踪code-push的执行,在Staging中实际上是和Production是一样的代码,可是当要热修复线上版本时,先会发布热更新到Staging版,在Staging测事后再经过promoting推到Production中去。android
大体步骤:ios
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
是要方法原生项目中去的。code-push deployment add MyAppIOS Test
,添加成功后,就能够经过code-push deployment ls MyAppIOS -k
来查看Test
部署环境下的key
了。常用code-push --h来查看能够执行的操做git
最后结果以下图所示:
github
在上面有提过须要把部署的key
添加到原生项目中,这样在不一样的运行环境下动态的使用对应的部署key
,例如在Staging
下使用Staging
的key
,在Relase
下使用Production
的key
,在Debug
下不使用热更新(如需在debug环境下测试code-push,能够在codePush.sync里的option参数中动态修改部署key
)。npm
在Android中动态部署key,而且在同一设备同时安装不一样部署的Android包
有两种方式:react-native
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的使用:tools.android.com/tech-docs/n…api
在iOS中动态部署key
官方配置入口:github.com/Microsoft/r…promise
在iPhone上同时安装相同App的不一样部署包
让你的iOS应用在不一样状态(debug, release)有不一样的图标和标题
一、分清楚 Target binary version 和 Label
label
表明发布的更新版本,
Target binary version
表明app的版本号。
二、使用patch打补丁,修改元数据属性。
使用场景:例如当你已经发布了一个更新,可是到有些状况下,好比--des
须要修改,--targetBinaryVersion
写错了,好比个人8.6.0
写成了8.6
,而后在我发布8.6.1
新版的时候就会拉取8.6
的版本更新,这个时候就能够code-push patch MyAppAndroid Production --label v4 --targetBinaryVersion 8.6.1
。
三、使用promote将Staging推到Production
使用场景:当你在指定的部署环境下测试更新时,例如Staging
,测试经过后,想把这个更新发布到正式生产环境Production
中,则可使用code-push promote MyAppAndroid Staging Production
,这时能够修改一些元数据,例如--description
、--targetBinaryVersion
、--rollout
等。
四、使用rollback回滚
使用场景:当你发布的更新测试没经过时,能够回滚到以前的某个版本。code-push rollback MyAppAndroid Production
,当执行这个命令时它会在MyAppAndroid上的Production部署上再次发布一个release,这个release的代码和元属性与Production上倒数第二个版本一致。也能够经过可选参数--targetRelease
来指定rollback
到的版本,例如code-push rollback MyAppAndroid Production --targetRelase v2
,则会新建一个release,这个release的代码和元属性与v2
相同。
注意:这个回滚是主动回滚,与自动回滚不同
五、使用debug查看是否使用了热更新版本
使用场景:当你想知道code-push的状态时,好比正在检查是否有更新包,正在下载,正在安装,当前加载的
bundle路径等,对于android可使用code-push debug android
,对于iOS可使用code-push debug ios
注意:debug ios必须在模拟器下才可使用
六、使用deployment h查看更新状态
使用场景:在发布更新后,须要查看安装状况,能够经过code-push deployment h MyAppAndroid Production
来查看每一次更新的安装指标。
七、较难理解的发布参数
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
不是强制要更新的。
false
,顾名思义,这个参数的意思就是这个更新包是否让用户使用,若是为true,则不会让用户下载这个更新包,使用场景:
code-push promote MyAppAndroid Staging Production --disabled false
来发布更新到正式环境,在对外公布信息后,使用code-push patch MyAppAndroid Production --disabled true
来让用户可使用这个更新。rollout
值小于100
,有三点要注意:
rollout
值被patch
为100
。rollback
时,rollout
值会被置空(为100)。promote
去其余部署时,rollout
会被置空(为100),能够从新指定--rollout
。八、理解安装指标(Install Metrics)数据
先来看下试用过程,如今有两个机子,分别为A和B
第一步:发了一个更新包,Install Metrics
中提示No install recorded
表示没有安装记录
v1
打了个
patch
,把
App Version
改成
1.0.0
,而且把元属性
Disabled
改成
true
Install Metrics
中的
Activite
为
0%
了(0 of 1),证实在
of
左边的数是会增降的,of右边的数是只会增不会降的,
of
左边的数表明当前
install
或者
receive
的总人数,当有用户卸载App,或者使用了更新的更新包时,这个数就会下降。所以它很好的解释了当前更新包有多少活跃用户,多少用户接收过这个安装包。
Install Metrics
中的
total
并无改变,仍是为
1
,表明有多少个用户
install
过这个更新包,这个数字只增不降,注意
total
与
active
的区别。
disabled
为
true
,所以不会接收这个更新包。
Disabled
改成
true
,让B
check update
,发现下图中
active
中
of
右边的数增长了
1
,表明多了一个用户
received
v1,可是
of
左边的数字为
0
,表明v1没有活跃用户,
total
的改变是多了
(1 pending)
,表明有一个用户
received
v1,可是尚未
install
(也就是
notifyApplicationReady
没被调用)
check update
,发现
Active
没有任何改变,由于B之前就接收过v1。
total
中
pending
数为
2
了,表明有两个用户
received
v1。
install
v1,
active
变为
50%
,能够看出
installed/received
为50%。
total
增长了
1
,表明v1多了一次
installed
,一共经历了
2
次
installed
,
(1 pending)
表明还有一个
received
。
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
为:
check update
,而且把
code-push debug ios
打开(注意debug必须使用模拟器)。发现v2的
total
直接从v1
total
中读下来,也就是说全部的v1用户都会
received
v2,
pending
为
1
表明A
recevied
v2,但没有
installed
。
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.
通过上面的测试,大体了解了Install metrics
中各个参数的意思,这里大概总结一下:
installed
这个release或者离开这个release(installed了别的更新包,或者卸载了App),总之有它就知道当前release的活跃用户量installed
这个release的用户的数量,这个数量只会增不会减。installed
,所以这一个数值会在release被下载时增加,在installed
时下降。这个指标主要是适配于没有为更新配置立马安装(mandatory)。若是你为更新配置了立马安装可是仍是有pending,颇有多是你的App启动时没有调用notifyApplicationReady
。installing
中发生crash
,code-push将会把它回滚到以前的一个更新包中。
检查、下载、使用以及rollback更新包
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) };
//会去判断这个包是不是已经安装失败的包(rollback过)
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;复制代码
拿到remotePackage后判断这个更新包是否能使用,能使用就去下载:
//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;
};复制代码
原生模块(以Android端为例):
首先寻找jsbundle路径,getJSBundleFile
中返回了CodePush.getJSBundleFile()
,在这里面会判断是否有新下载的更新包,若是比本地新则加载这个更新包,不然加载本地包,
//CodePush.java 143行
public String getJSBundleFileInternal(String assetsBundleFileName) {
this.mAssetsBundleFileName = assetsBundleFileName;
String binaryJsBundleUrl = CodePushConstants.ASSETS_BUNDLE_PREFIX + assetsBundleFileName;
//获取当前可使用的更新包的路径
String packageFilePath = mUpdateManager.getCurrentPackageBundlePath(this.mAssetsBundleFileName);
if (packageFilePath == null) {
// 当前没有任何更新包可使用
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
sIsRunningBinaryVersion = true;
return binaryJsBundleUrl;
}
//获取当前可使用的更新包的配置文件
JSONObject packageMetadata = this.mUpdateManager.getCurrentPackage();
if (isPackageBundleLatest(packageMetadata)) {
//若是当前更新包是最新可用的(版本号相符),使用当前更新包
CodePushUtils.logBundleUrl(packageFilePath);
sIsRunningBinaryVersion = false;
return packageFilePath;
} else {
// 当前App的版本是新的(好比更新包是8.6.0的,如今App是8.6.1)
this.mDidUpdate = false;
if (!this.mIsDebugMode || hasBinaryVersionChanged(packageMetadata)) {
//当App版本号有改变的时候清除全部更新包
this.clearUpdates();
}
//使用本地bundle
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
sIsRunningBinaryVersion = true;
return binaryJsBundleUrl;
}
}复制代码
在js端的remotePackage.download
中会调用原生的downloadUpdate方法
:
//CodePushNativeModule.java 203行
public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {
//后台下载任务
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try {
JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage);
CodePushUtils.setJSONValueForKey(mutableUpdatePackage, CodePushConstants.BINARY_MODIFIED_TIME_KEY, "" + mCodePush.getBinaryResourcesModifiedTime());
//开始下载remotePackage
mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
//下载进度回调
...
});
//获取remotePackage的信息并返回给js
JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));
} catch (IOException e) {
e.printStackTrace();
promise.reject(e);
} catch (CodePushInvalidUpdateException e) {
e.printStackTrace();
mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage));
promise.reject(e);
}
return null;
}
};
asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}复制代码
在js端调用installUpdate
,一共会出现三个hash值,分别是刚下载的更新包的hash值(packageHash),当前使用的hash值(currentPackageHash),之前使用的hash值(previousPackageHash),如今要把prevousPackageHash = currentPackageHash
,currentPackageHash = packageHash
//CodePushUpdateManager.java
public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) {
//获取更新包的hash值
String packageHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
JSONObject info = getCurrentPackageInfo();
//获取当前使用的更新包的hash值
String currentPackageHash = info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null);
if (packageHash != null && packageHash.equals(currentPackageHash)) {
// 若是下载的更新包和当前使用的是同一个更新包,不作处理
return;
}
if (removePendingUpdate) {
//若是当前使用的更新包是下载好但没有installed的更新包,则把这个更新包移除
String currentPackageFolderPath = getCurrentPackageFolderPath();
if (currentPackageFolderPath != null) {
FileUtils.deleteDirectoryAtPath(currentPackageFolderPath);
}
} else {
//获取以前的更新包,并移除
String previousPackageHash = getPreviousPackageHash();
if (previousPackageHash != null && !previousPackageHash.equals(packageHash)) {
FileUtils.deleteDirectoryAtPath(getPackageFolderPath(previousPackageHash));
}
//将上一个更新包指向当前更新包
CodePushUtils.setJSONValueForKey(info, CodePushConstants.PREVIOUS_PACKAGE_KEY, info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null));
}
//设置当前可以使用的更新包为update package
CodePushUtils.setJSONValueForKey(info, CodePushConstants.CURRENT_PACKAGE_KEY, packageHash);
updateCurrentPackageInfo(info);
}复制代码
将刚下载的更新包标记为pending package,isloading为false:
//CodePushNativeModule.java 411行,
//标记为pending,而且isLoading为false
mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false);复制代码
App第一次进入和从新加载bundle时会调用initializeUpdateAfterRestart
,用来判断是否有pending package,若是有而且isloading为true(被init过),表明这个pending package在notifyApplicationReady
前崩溃了,所以须要rollback,若是isloading为false则表明是第一次加载更新包,会将isloading(init)置为true,用来判断下次进入时需不须要rollback:
//CodePush.js 177行
void initializeUpdateAfterRestart() {
...
JSONObject pendingUpdate = mSettingsManager.getPendingUpdate();
if (pendingUpdate != null) {
//有新的更新包可用
JSONObject packageMetadata = this.mUpdateManager.getCurrentPackage();
if (!isPackageBundleLatest(packageMetadata) && hasBinaryVersionChanged(packageMetadata)) {
//版本不符
CodePushUtils.log("Skipping initializeUpdateAfterRestart(), binary version is newer");
return;
}
try {
boolean updateIsLoading = pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY);
if (updateIsLoading) {
// Pending package已经被init过, 可是 notifyApplicationReady 没有被调用.
// 所以认为这是个无效的更新而且rollback.
CodePushUtils.log("Update did not finish loading the last time, rolling back to a previous version.");
sNeedToReportRollback = true;
rollbackPackage();
} else {
// 如今有个新的更新包能够运行,开始init这个更新包
//若是它崩溃了,须要在下一次启动时rollback
mSettingsManager.savePendingUpdate(pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY),
/* isLoading */true);
}
} catch (JSONException e) {
// Should not happen.
throw new CodePushUnknownException("Unable to read pending update metadata stored in SharedPreferences", e);
}
}
}复制代码
rollback的代码:
//CodePush.java 257行
private void rollbackPackage() {
//将当前使用的更新包标记为失败的包
JSONObject failedPackage = mUpdateManager.getCurrentPackage();
mSettingsManager.saveFailedUpdate(failedPackage);
//用以前使用的更新包替换当前使用的更新包
mUpdateManager.rollbackPackage();
//移除pending package
mSettingsManager.removePendingUpdate();
}复制代码
notifyApplicationReady的代码:
//CodePushNativeModule.java 498行
public void notifyApplicationReady(Promise promise) {
//移除pending package
mSettingsManager.removePendingUpdate();
promise.resolve("");
}复制代码
总结:
js端使用checkupdate
用App当前的版本号,当时使用的更新包信息以及部署key传递给原生,原生调用codu-push服务器查询是否有更新包可使用,若是不存在更新包,或者更新包与当前使用的更新包一致,或者版本号不符都不会产生remotePackage。拿到remotePackage后会去原生的本地存储查询这个remotePackage的hash是否为failedPackage,若是是failedPackage则会选择忽略这个更新包,不然就download这个更新包。
下载好更新包后,将这个更新包标志位pending package,而且isloading为false,将previousPacakge置为currentPackage,currentPackage置为下载的更新包。
在加载更新包时会判断这个更新包是不是pending package,若是是则判断isloading是否为false,若是为false则表明这个pending package是第一次加载,若是为true则表明这个pending被加载后调用notifyApplicationReady前发生崩溃,须要回滚。
若是发生回滚会将pending package置空,将previouPackage赋值给currentPackage。
在正确加载更新包后,应该手动触发notifyApplicationReady将pending package置空,表明这个更新包被正确installed。
示例:
hash包的管理:
failed package:崩溃的package
pending package:下载好的没有被installed的package
previous package: 以前使用的package
current package:当前正在使用package
第一步:下载更新包A
pending pacakge = A
isloding = false
previous package = current package
current package = pending package复制代码
第二步:第一次使用A
pending isloading = true复制代码
若是在notifyApplicationReady以前发生崩溃走第三步,不然走第四步。
第三步:再次加载bundle,发现pending package还存在,而且isloading为true,回滚
第四步:pending package不存在,不作任何处理