ReactNative热更新过程梳理

前言:前面写了如何搭建热更新的本地环境,今天梳理一下热更新的流程,在看代码之前,我们先想一下,实现更新应该需要以下几个步骤:
1、向服务端提交检查请求
2、服务端告知本地有新版本,通知本地去更新
3、本地下载新版本
4、安装新版本
5、加载新版本内容
以上应该是检查更新的基本步骤,我们再来看一下对应的代码:
demo中的检查更新是在App.js文件中执行的,我们可以看到执行更新主要有以下两个方法:

/** Update is downloaded silently, and applied on restart (recommended) */
  sync() {
    CodePush.sync(
      {},
      this.codePushStatusDidChange.bind(this),
      this.codePushDownloadDidProgress.bind(this)
    );
  }

  /** Update pops a confirmation dialog, and then immediately reboots the app */
  syncImmediate() {
    CodePush.sync(
      { installMode: CodePush.InstallMode.IMMEDIATE, updateDialog: true },
      this.codePushStatusDidChange.bind(this),
      this.codePushDownloadDidProgress.bind(this)
    );
  }

方法中都调用了CodePush.sync这个方法,我们看一下这个方法做了什么:

/**
     * Allows checking for an update, downloading it and installing it, all with a single call.
检查更新、下载、安装都在这个方法里啦啦啦啦啦。
     *
     * @param options Options used to configure the end-user update experience (e.g. show an prompt?, install the update immediately?).
  更新的参数
     * @param syncStatusChangedCallback An optional callback that allows tracking the status of the sync operation, as opposed to simply checking the resolved state via the returned Promise.
更新状态改变的回调
     * @param downloadProgressCallback An optional callback that allows tracking the progress of an update while it is being downloaded.
下载进度的回调
     * @param handleBinaryVersionMismatchCallback An optional callback for handling target binary version mismatch
版本不一致的回调(可选)
     */
    function sync(options?: SyncOptions,
       syncStatusChangedCallback?: SyncStatusChangedCallback, 
       downloadProgressCallback?: DowloadProgressCallback, 
       handleBinaryVersionMismatchCallback?: 
       HandleBinaryVersionMismatchCallback): Promise<SyncStatus>;

检查更新的参数:

export interface SyncOptions {
    deploymentKey?: string;
    检查更新的部署密钥,就是我们在code-push服务器上创建app时生成的key,服务器根据此来判断是检查哪一个应用。

    installMode?: CodePush.InstallMode;//安装模式,默认是下一次restart时安装,包含:
    IMMEDIATE(安装更新并立刻重启应用),
    ON_NEXT_RESTART(安装更新,但不立马重启,直到下一次重新进入),
    ON_NEXT_RESUME(安装更新,但是不立马重新启动,直到下一次从后台恢复到前台)
    ON_NEXT_SUSPEND(下一次处于后台时)
  
    mandatoryInstallMode?: CodePush.InstallMode;
    指定如果更新的版本是标识了强制更新怎么更新。具体参数和上面一样。

    minimumBackgroundDuration?: number;
    指定应用程序需要在后台重新启动应用程序之前所需的最小秒数,默认是0。

    updateDialog?: UpdateDialog;
    标记检查到更新是要弹出对话框
}

让我们再看看CodePush中sync方法具体做了什么处理(代码在node_modules/react-native-code-push/CodePush.js,本地的module代码对应的是react-native-code-push对应的lib库中的com/microsoft/codepush/react/CodePushNativeModule.java):

const sync = (() => {
  let syncInProgress = false;
  const setSyncCompleted = () => { syncInProgress = false; };
  //上面是互斥处理,保证多次调用sync只有一个在检查更新,其他直接返回正在检查中的状态
  return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
    let syncStatusCallbackWithTryCatch, downloadProgressCallbackkWithTryCatch;
    //将syncStatusChangeCallback和downloadProgressCallback放入try catch中。
    if (typeof syncStatusChangeCallback === "function") {
      syncStatusCallbackWithTryCatch = (...args) => {
        try {
          syncStatusChangeCallback(...args);
        } catch (error) {
          log(`An error has occurred : ${error.stack}`);
        }
      }
    }
    //将syncStatusChangeCallback和downloadProgressCallback放入try catch中。
    if (typeof downloadProgressCallback === "function") {
      downloadProgressCallbackkWithTryCatch = (...args) => {
        try {
          downloadProgressCallback(...args);
        } catch (error) {
          log(`An error has occurred: ${error.stack}`);
        }
      }
    }
    //检查更新的互斥逻辑处理
    if (syncInProgress) {
      typeof syncStatusCallbackWithTryCatch === "function"
        ? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
        : log("Sync already in progress.");
      return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
    }

    syncInProgress = true;
   //调用了syncInternal方法
    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackkWithTryCatch, handleBinaryVersionMismatchCallback);
    syncPromise
      .then(setSyncCompleted)
      .catch(setSyncCompleted);

    return syncPromise;
  };
})();

我们再看一下syncInternal方法做了什么,这个里面就包含了所有的流程:

/*
该方法提供了一个简单的在线合并检查、下载、安装更新
 * The syncInternal method provides a simple, one-line experience for
 * incorporating the check, download and installation of an update.
 *
它将这一些方法组合到一起,并支持强制更新、忽略失败更新、更新确认dialog等
 * It simply composes the existing API methods together and adds additional
 * support for respecting mandatory updates, ignoring previously failed
 * releases, and displaying a standard confirmation UI to the end-user
 * when an update is available.
 */
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
  let resolvedInstallMode;
  //准备检查更新的参数
  const syncOptions = {
    deploymentKey: null,//这个地方为null,是因为从原生代码中获取的
    ignoreFailedUpdates: true,//是否忽略失败的更新版本
    installMode: CodePush.InstallMode.ON_NEXT_RESTART,//安装模式
    mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,//强制更新时的安装模式
    minimumBackgroundDuration: 0,//重启最小等待时间
    updateDialog: null,//是否要对话框
    ...options//其他传递过来的参数
  };
//更新状态同步的回调
  syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
    ? syncStatusChangeCallback
    : (syncStatus) => {
        switch(syncStatus) {
          case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
            break;
          case CodePush.SyncStatus.AWAITING_USER_ACTION:
            break;
          case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
            break;
          case CodePush.SyncStatus.INSTALLING_UPDATE:
            break;
          case CodePush.SyncStatus.UP_TO_DATE:
            break;
          case CodePush.SyncStatus.UPDATE_IGNORED:
            break;
          case CodePush.SyncStatus.UPDATE_INSTALLED:
            if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
            } else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
            }
            break;
          case CodePush.SyncStatus.UNKNOWN_ERROR:
            break;
        }
      };

  try {
    //通知App已经ready。
    await CodePush.notifyApplicationReady();
   //发出第一个同步状态。
    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
//检查更新
    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
//定义下载的方法
    const doDownloadAndInstall = async () => {
     ...
    };

    const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
    if (!remotePackage || updateShouldBeIgnored) {
     //如果没有更新或者当前更新应该被忽略的

      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 if (syncOptions.updateDialog) {
//如果需要对话框,则需要用户手动点击下载安装
     ...

      return await new Promise((resolve, reject) => {
        let message = null;
        const dialogButtons = [{
          text: null,
          onPress:() => {
            doDownloadAndInstall()
              .then(resolve, reject);
          }
        }];
    } else {
//如果不需要对话框,直接下载并安装
      return await doDownloadAndInstall();
    }
  } catch (error) {
    syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
    log(error.message);
    throw error;
  }
};

notifyApplicationReady

上面的notifyApplicationReady方法是调用了notifyApplicationReadyInternal方法,而notifyApplicationReadyInternal方法如下:

async function notifyApplicationReadyInternal() {
  await NativeCodePush.notifyApplicationReady();//原生Module的方法
//获取本地的状态,并上传到服务器
  const statusReport = await NativeCodePush.getNewStatusReport();
  statusReport && tryReportStatus(statusReport); // Don't wait for this to complete.
  return statusReport;
}

NativeCodePush.notifyApplicationReady()原生代码操作如下:

@ReactMethod
    public void notifyApplicationReady(Promise promise) {
        mSettingsManager.removePendingUpdate();//清除未更新的hash之
        promise.resolve("");
    }

NativeCodePush.getNewStatusReport的方法就是根据上一次检查的状态返回不同的值,此处不做细述。
tryReportStatus方法如下:

async function tryReportStatus(statusReport, resumeListener) {
    log(`statusReport =  (${statusReport})`);
  const config = await getConfiguration();//获取配置信息
  const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;//当前版本的标签或者app的版本
  const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;//部署的key
  try {
    if (statusReport.appVersion) {

      const sdk = getPromisifiedSdk(requestFetchAdapter, config);//得到操作的sdk,具体方法在前一个目录下code-push/script/acquisition-sdk.js
      await sdk.reportStatusDeploy(/* deployedPackage */ null, /* status */ null, previousLabelOrAppVersion, previousDeploymentKey);
    } else {
      const label = statusReport.package.label;
      if (statusReport.status === "DeploymentSucceeded") {
        log(`Reporting CodePush update success (${label})`);
      } else {
        log(`Reporting CodePush update rollback (${label})`);
      }

      config.deploymentKey = statusReport.package.deploymentKey;
      const sdk = getPromisifiedSdk(requestFetchAdapter, config);
      await sdk.reportStatusDeploy(statusReport.package, statusReport.status, previousLabelOrAppVersion, previousDeploymentKey);
    }
    NativeCodePush.recordStatusReported(statusReport);//本地保存状态报告
    resumeListener && AppState.removeEventListener("change", resumeListener);//移除对于change时间的监听
  } catch (e) {
    log(`Report status failed: ${JSON.stringify(statusReport)}`);
    NativeCodePush.saveStatusReportForRetry(statusReport);//本地保存状态报告并在resume时重新调用tryReportStatus方法
    // Try again when the app resumes
    if (!resumeListener) {
      resumeListener = async (newState) => {
        if (newState !== "active") return;
        const refreshedStatusReport = await NativeCodePush.getNewStatusReport();
        if (refreshedStatusReport) {
          tryReportStatus(refreshedStatusReport, resumeListener);
        } else {
          AppState.removeEventListener("change", resumeListener);
        }
      };
      AppState.addEventListener("change", resumeListener);
    }
  }
}

notifyApplicationReady方法讲解完成

checkForUpdate

状态什么的处理完后就开始执行checkForUpdate检查更新了。

async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
  /*
   //获取本地配置信息,最终调用NativeCodePush.getConfiguration方法
   */
  const nativeConfig = await getConfiguration();

  const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
  const sdk = getPromisifiedSdk(requestFetchAdapter, config);//和服务器交互的sdk
  const localPackage = await module.exports.getCurrentPackage();//本地的package

  let queryPackage;
  if (localPackage) {
    queryPackage = localPackage;
  } else {
    queryPackage = { appVersion: config.appVersion };
    if (Platform.OS === "ios" && config.packageHash) {
      queryPackage.packageHash = config.packageHash;
    }
  }
  //从服务器检查更新
  const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);

  /*
   * 有四种结果:
   * ----------------------------------------------------------------
   * 1) 没有更新.
   * 2) 服务器又更新,但是当前原生版本不支持此版本
  
   * 3) 服务器告诉又更新,但是hsah值和本地是一样的(理论上不会出现)
   * 4) 服务器有更新,但是hash值和当前运行的二进制(差异化的二进制hash)是一样的,这只会发生在Android手机,我理解的是iOS没有返回差异化的二进制。
   */
  if (!update || update.updateAppVersion ||
      localPackage && (update.packageHash === localPackage.packageHash) ||
      (!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
    if (update && update.updateAppVersion) {
     //有更新,但是当前原生的版本不兼容
      if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === "function") {
        handleBinaryVersionMismatchCallback(update)
      }
    }

    return null;
  } else {
//和本地对比,返回此更新的信息
    const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
    remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);//根据此hash判断是否是失败的更新(本地会存储之前更新的状态)
    remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
    return remotePackage;
  }
}

需要有两点需要注意:

1、remotePackage不仅仅有此版本的信息,同时也将PackageMixins的属性包含进来了,而PackageMixins对应的是package-mixins.js的代码,里面只有两个属性download下载更新和install安装更新。里面的逻辑在讲到下载时再细说。

2、上面提到的和服务器交互的sdkcode-push/script/acquisition-sdk.js里面有queryUpdateWithCurrentPackage查询更新,reportStatusDeploy记录状态报告,reportStatusDownload下载状态报告,这几个方法,具体细节可以看源代码,不复杂。

到目前为止,我们已经得到了更新的版本信息。往下执行会根据此版本信息来判断是否是忽略版本,需要下载。如果需要下载,则会执行前面提到的doDownloadAndInstall方法。

doDownloadAndInstall

doDownloadAndInstall里面代码逻辑很少,如下:

const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);//同步状态
      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;
    };

我们在来分布解析下载和安装的逻辑,

download

download的逻辑比较简单:

async download(downloadProgressCallback) {
        if (!this.downloadUrl) {//下载地址为空,抛出异常
          throw new Error("Cannot download an update without a download url");
        }

        let downloadProgressSubscription;//下载进度回调
        if (downloadProgressCallback) {
          const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
          // CodePushDownloadProgress该事件会有原生下载时发出
          downloadProgressSubscription = codePushEventEmitter.addListener(
            "CodePushDownloadProgress",
            downloadProgressCallback
          );
        }

        // Use the downloaded package info. Native code will save the package info
        // so that the client knows what the current package version is.
        try {
          const updatePackageCopy = Object.assign({}, this);
          Object.keys(updatePackageCopy).forEach((key) => (typeof updatePackageCopy[key] === 'function') && delete updatePackageCopy[key]);
//调用本地下载
          const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
//记录下载状态
          reportStatusDownload && reportStatusDownload(this);
//返回下载package信息
          return { ...downloadedPackage, ...local };
        } finally {
          downloadProgressSubscription && downloadProgressSubscription.remove();
        }
      },

      isPending: false // A remote package could never be in a pending state
    };

上面NativeCodePush.downloadUpdate是通过本地下载,本地的源码如下:

@ReactMethod
    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());
//通过    mUpdateManager下载并监听进度回调,具mUpdateManager的方法和普通下载一样,不多细述。           
mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
                        private boolean hasScheduledNextFrame = false;
                        private DownloadProgress latestDownloadProgress = null;

                        @Override
                        public void call(DownloadProgress downloadProgress) {
                            if (!notifyProgress) {
                                return;
                            }

                            latestDownloadProgress = downloadProgress;
                            // If the download is completed, synchronously send the last event.
                            if (latestDownloadProgress.isCompleted()) {
                                dispatchDownloadProgressEvent();
                                return;
                            }

                            if (hasScheduledNextFrame) {
                                return;
                            }

                            hasScheduledNextFrame = true;
                            getReactApplicationContext().runOnUiQueueThread(new Runnable() {
                                @Override
                                public void run() {
                                    ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new ChoreographerCompat.FrameCallback() {
                                        @Override
                                        public void doFrame(long frameTimeNanos) {
                                            if (!latestDownloadProgress.isCompleted()) {
                                                dispatchDownloadProgressEvent();
                                            }

                                            hasScheduledNextFrame = false;
                                        }
                                    });
                                }
                            });
                        }
//重点是该方法,通过时间不断地发出下载的进度状态
                        public void dispatchDownloadProgressEvent() {
                            getReactApplicationContext()
                                    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                                    .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
                        }
                    }, mCodePush.getPublicKey());

                    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);
    }

通过上面源码分析一下就可以知道下载流程,还是比较简单的,重点说一下通知下载进度给ReactNative的Component是通过getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());方法,而时间的event就是CodePushDownloadProgress,这个上面已经提到过了。
下载完成之后就执行到

// 安装模式
      resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;

      syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
//准备安装
      await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
        syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
      });

方法了,我们再来看一下具体的安装方法源码:

const local = {
//传入参数 
installMode安装模式,默认NativeCodePush.codePushInstallModeOnNextRestart,
minimumBackgroundDuration上面提到的,如果是onresume的安装模式,应用从上一次暂停到前台的时间间隔开始安装,默认为0。
updateInstalledCallback安装回调。
    async install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, minimumBackgroundDuration = 0, updateInstalledCallback) {
      const localPackage = this;
      const localPackageCopy = Object.assign({}, localPackage); // In dev mode, React Native deep freezes any object queued over the bridge
//调用本地的installUpdate方法,就是讲bundle文件准备好,以便重新加载,后面细述。
      await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
      updateInstalledCallback && updateInstalledCallback();
      if (installMode == NativeCodePush.codePushInstallModeImmediate) {
//如果安装模式是立即安装的话,直接重启
        RestartManager.restartApp(false);
      } else {
//如果不是,清除已经准备好的重启任务
        RestartManager.clearPendingRestart();
        localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
      }
    },

    isPending: false // A local package wouldn't be pending until it was installed
  };

上面是CodePush中的安装方法,里面有两个点需要细述:

一、本地的NativeCodePush.installUpdate方法:

@ReactMethod
    public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) {
        AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                mUpdateManager.installPackage(CodePushUtils.convertReadableToJsonObject(updatePackage), mSettingsManager.isPendingUpdate(null));

                String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY);
                if (pendingHash == null) {
                    throw new CodePushUnknownException("Update package to be installed has no hash.");
                } else {
                    mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false);
                }

                if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue() ||
                    // We also add the resume listener if the installMode is IMMEDIATE, because
                    // if the current activity is backgrounded, we want to reload the bundle when
                    // it comes back into the foreground.
                    installMode == CodePushInstallMode.IMMEDIATE.getValue() ||
                    installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue()) {

                    // Store the minimum duration on the native module as an instance
                    // variable instead of relying on a closure below, so that any
                    // subsequent resume-based installs could override it.
                    CodePushNativeModule.this.mMinimumBackgroundDuration = minimumBackgroundDuration;

                    if (mLifecycleEventListener == null) {
                        // Ensure we do not add the listener twice.
                        mLifecycleEventListener = new LifecycleEventListener() {
                            private Date lastPausedDate = null;
                            private Handler appSuspendHandler = new Handler(Looper.getMainLooper());
                            private Runnable loadBundleRunnable = new Runnable() {
                                @Override
                                public void run() {
                                    CodePushUtils.log("Loading bundle on suspend");
                                    loadBundle();
                                }
                            };

                            @Override
                            public void onHostResume() {
                                appSuspendHandler.removeCallbacks(loadBundleRunnable);
                                // As of RN 36, the resume handler fires immediately if the app is in
                                // the foreground, so explicitly wait for it to be backgrounded first
                                if (lastPausedDate != null) {
                                    long durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000;
                                    if (installMode == CodePushInstallMode.IMMEDIATE.getValue()
                                            || durationInBackground >= CodePushNativeModule.this.mMinimumBackgroundDuration) {
                                        CodePushUtils.log("Loading bundle on resume");
                                        loadBundle();
                                    }
                                }
                            }

                            @Override
                            public void onHostPause() {
                                // Save the current time so that when the app is later
                                // resumed, we can detect how long it was in the background.
                                lastPausedDate = new Date();

                                if (installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue() && mSettingsManager.isPendingUpdate(null)) {
                                    appSuspendHandler.postDelayed(loadBundleRunnable, minimumBackgroundDuration * 1000);
                                }
                            }

                            @Override
                            public void onHostDestroy() {
                            }
                        };

                        getReactApplicationContext().addLifecycleEventListener(mLifecycleEventListener);
                    }
                }

                promise.resolve("");

                return null;
            }
        };

        asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

这个里面有两个知识点:
1、是调用mUpdateManager.installPackage来处理已经下载好的bundle文件,里面原理比较简单,可以自行看一下。每一个版本的文件命名都是以其hash值命名的。
2、建立一个监听,根据安装模式来判断是在onHostResume中调用loadBundle还是在onHostPause中调用loadBundle

二、RestartManager这个类:
RestartManager这个类的源码在RestartManager.js中,里面有两个要素,一个是重启任务的数组,这个不用细说;一个是重启的方法,也是调用NativeCodePush.restartApp(onlyIfUpdateIsPending)原生的方法,源码如下:

 @ReactMethod
    public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) {
        // If this is an unconditional restart request, or there
        // is current pending update, then reload the app.
        if (!onlyIfUpdateIsPending || mSettingsManager.isPendingUpdate(null)) {
            loadBundle();
            promise.resolve(true);
            return;
        }

        promise.resolve(false);
    }

我们看到了里面关键的方法是loadBundle,这个方法获取了一个ReactInstanceManager对象和最后更新下来的bundle文件路径latestJSBundleFile,然后调用setJSBundle(instanceManager, latestJSBundleFile)方法完成重启,这个方法源码如下:

private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile) throws IllegalAccessException {
        try {
            JSBundleLoader latestJSBundleLoader;
//准备JSBundleLoader对象
            if (latestJSBundleFile.toLowerCase().startsWith("assets://")) {
                latestJSBundleLoader = JSBundleLoader.createAssetLoader(getReactApplicationContext(), latestJSBundleFile, false);
            } else {
                latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
            }
//通过反射,得到 instanceManager中的mBundleLoader对象
            Field bundleLoaderField = instanceManager.getClass().getDeclaredField("mBundleLoader");
            bundleLoaderField.setAccessible(true);
          //给instanceManager中重新设置mBundleLoader对象
            bundleLoaderField.set(instanceManager, latestJSBundleLoader);
        } catch (Exception e) {
            CodePushUtils.log("Unable to set JSBundle - CodePush may not support this version of React Native");
            throw new IllegalAccessException("Could not setJSBundle");
        }
    }

上面提到的通过ReactInstanceManager中的JSBundleLoader完成bundle文件的重加载,具体的源码就不多分析,后续有时间可以梳理一下ReactNative如何加载bundle文件的文章。重新加载完成之后,通过调用instanceManager.recreateReactContextInBackground();重新创建了ReactContext。

至此,一次完整的检查--下载--安装--更新流程完成了。

需要注意以下几个知识点:
1、ReactNative项目在进入后台是会发出background事件通知,在进入前台时会发出active事件通知。
2、热更新检查可以通过手动调用sync方法也可以在主界面的componentDidMount中通过事件触发调用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容