之前写了一篇关于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)有不同的图标和标题
参考项目:https://github.com/lyxia/CodePushExp
应用中经常遇到的技巧
1、分清楚 Target binary version 和 Label
label
代表发布的更新版本,
Target binary version
代表app的版本号。
2、使用patch打补丁,修改元数据属性。
使用场景:例如当你已经发布了一个更新,但是到有些情况下,比如--des
需要修改,--targetBinaryVersion
写错了,比如我的8.6.0
写成了8.6
,然后在我发布8.6.1
新版的时候就会拉取8.6
的版本更新,这个时候就可以code-push patch MyAppAndroid Production --label v4 --targetBinaryVersion 8.6.1
。
3、使用promote将Staging推到Production
使用场景:当你在指定的部署环境下测试更新时,例如Staging
,测试通过后,想把这个更新发布到正式生产环境Production
中,则可以使用code-push promote MyAppAndroid Staging Production
,这时可以修改一些元数据,例如--description
、--targetBinaryVersion
、--rollout
等。
4、使用rollback回滚
使用场景:当你发布的更新测试没通过时,可以回滚到之前的某个版本。code-push rollback MyAppAndroid Production
,当执行这个命令时它会在MyAppAndroid上的Production部署上再次发布一个release,这个release的代码和元属性与Production上倒数第二个版本一致。也可以通过可选参数--targetRelease
来指定rollback
到的版本,例如code-push rollback MyAppAndroid Production --targetRelase v2
,则会新建一个release,这个release的代码和元属性与v2
相同。
注意:这个回滚是主动回滚,与自动回滚不一样
5、使用debug查看是否使用了热更新版本
使用场景:当你想知道code-push的状态时,比如正在检查是否有更新包,正在下载,正在安装,当前加载的
bundle路径等,对于android可以使用code-push debug android
,对于iOS可以使用code-push debug ios
注意:debug ios必须在模拟器下才可以使用
6、使用deployment h查看更新状态
使用场景:在发布更新后,需要查看安装情况,可以通过code-push deployment h MyAppAndroid Production
来查看每一次更新的安装指标。
7、较难理解的发布参数
- 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
。
- 不能发布新的更新包,除非最后一个更新包的
8、理解安装指标(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将会把它回滚到之前的一个更新包中。
可以在https://github.com/lyxia/CodePushExp运行示例。
源码解读
检查、下载、使用以及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 asyncTask = new AsyncTask() {
@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不存在,不做任何处理
Demo
地址:https://github.com/lyxia/CodePushExp