react-native-code-push进阶篇

8,223 阅读17分钟

之前写了一篇关于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来查看可以执行的操作

最后结果如下图所示:

image.png
image.png

image.png
image.png

在原生项目中动态部署

在上面有提过需要把部署的key添加到原生项目中,这样在不同的运行环境下动态的使用对应的部署key,例如在Staging下使用Stagingkey,在Relase下使用Productionkey,在Debug下不使用热更新(如需在debug环境下测试code-push,可以在codePush.sync里的option参数中动态修改部署key)。

在Android中动态部署key,并且在同一设备同时安装不同部署的Android包
有两种方式:

  • 官方配置入口:github.com/Microsoft/r… you want to be able to install both debug and release builds simultaneously on the same device`中有提到在同一设备同时安装不同部署的Android包。
  • 第二种方式是通过资源文件R.string来实现同样的效果,在app/src中分别添加staging/res/valuesdebug/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…

在iOS中动态部署key
官方配置入口:github.com/Microsoft/r…

在iPhone上同时安装相同App的不同部署包
让你的iOS应用在不同状态(debug, release)有不同的图标和标题

参考项目:github.com/lyxia/CodeP…

应用中经常遇到的技巧

1、分清楚 Target binary version 和 Label

image.png
image.png

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时,updateDialogtrue的情况下,如果-mandatoryfalse,则更新提示框会弹出两个按钮,一个是【确认更新】,一个是【取消更新】,但是在-mandatorytrue的情况下就只有一个按钮【确认更新】用户没法拒绝安装这个更新。在updateDialogfalse的情况下,-mandatory 就不起作用了,因为都会静默更新。

    注意:mandatory是服务器传给客户端的,它是一个“动态”属性,意思就是当你正在使用版本v1的更新,然后现在服务器上有v2v3的更新可用,v2mandatorytrue,v3mandatoryfalse,此时去check update,服务器会返回v3的更新属性给客户端,这时服务返回的v3mandatorytrue,因为v3v2之后发布的更新,它会被认为是包含v2的所有更新信息的,竟然v2有强制更新的需求,那跳过v2直接更新到v3的情况下,v3也被要求强制更新。但是如果你当前是在使用v2的更新包,check update时服务器返回v3的更新包属性,此时v3mandatoryfalse,因为对于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值被patch100
    • rollback时,rollout值会被置空(为100)。
    • promote去其他部署时,rollout会被置空(为100),可以重新指定--rollout

8、理解安装指标(Install Metrics)数据
先来看下试用过程,现在有两个机子,分别为A和B
第一步:发了一个更新包,Install Metrics中提示No install recorded表示没有安装记录

image.png
image.png

第二步:A安装了这个更新包,并且现在正在使用这个更新包
image.png
image.png

第三步:给v1打了个patch,把App Version改为1.0.0,并且把元属性Disabled改为true
image.png
image.png

第四步:A卸掉App,发现Install Metrics中的Activite0%了(0 of 1),证明在of左边的数是会增降的,of右边的数是只会增不会降的,of左边的数代表当前install或者receive的总人数,当有用户卸载App,或者使用了更新的更新包时,这个数就会降低。因此它很好的解释了当前更新包有多少活跃用户,多少用户接收过这个安装包。Install Metrics中的total并没有改变,还是为1,代表有多少个用户install过这个更新包,这个数字只增不降,注意totalactive的区别。
image.png
image.png

第五步:分别在A、B上安装这个App。发现图中数据和上图没有任何区别,那是因为disabledtrue,因此不会接收这个更新包。
image.png
image.png

第六步:给v1打了个patch,把元属性Disabled改为true,让Bcheck update,发现下图中activeof右边的数增加了1,代表多了一个用户receivedv1,但是of左边的数字为0,代表v1没有活跃用户,total的改变是多了(1 pending),代表有一个用户receivedv1,但是还没有install(也就是notifyApplicationReady没被调用)
image.png
image.png

第七步:让Acheck update,发现Active没有任何改变,因为B以前就接收过v1。totalpending数为2了,代表有两个用户receivedv1。
image.png
image.png

第八步:让Binstallv1,active变为50%,可以看出installed/received为50%。total增加了1,代表v1多了一次installed,一共经历了2installed(1 pending)代表还有一个received
image.png
image.png

第九步:让Ainstallv1,active变为100%total增加了1,代表v1多了一次installed,一共经历了3installed,没有pending代表没有received
image.png
image.png

第十步:发一个可以触发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为:

image.png
image.png

这时我们让A去check update,并且把code-push debug ios打开(注意debug必须使用模拟器)。发现v2的total直接从v1total中读下来,也就是说所有的v1用户都会receivedv2,pending1代表Areceviedv2,但没有installed
image.png
image.png

这时,我们让Ainstalledv2,发现A会闪退,然后再次进入App,发现pending没有了,但是total并没有增加,active也没有改变,pending的加到rollbacks去了。
image.png
image.png

此时code-push debug ios会打印Update did not finish loading the last time, rolling back to a previous version.
第十一步:发布个修订版,修复v2产生的bug。然后让B安装。

image.png
image.png

哈哈,这个图看懂了吗,看懂了就代表了解它的意思了O(∩_∩)O哈哈~
第十二步:发布一个强制更新的更新包。

image.png
image.png

经过上面的测试,大致了解了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将会把它回滚到之前的一个更新包中。

    可以在github.com/lyxia/CodeP…

源码解读

检查、下载、使用以及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 = currentPackageHashcurrentPackageHash = 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

地址:github.com/lyxia/CodeP…

image.png
image.png

image.png
image.png

image.png
image.png

image.png
image.png