ReactNative熱更新過程梳理

前言:前面寫了如何搭建熱更新的本地環(huán)境,今天梳理一下熱更新的流程,在看代碼之前,我們先想一下,實(shí)現(xiàn)更新應(yīng)該需要以下幾個(gè)步驟:
1、向服務(wù)端提交檢查請求
2、服務(wù)端告知本地有新版本,通知本地去更新
3、本地下載新版本
4、安裝新版本
5、加載新版本內(nèi)容
以上應(yīng)該是檢查更新的基本步驟,我們再來看一下對應(yīng)的代碼:
demo中的檢查更新是在App.js文件中執(zhí)行的,我們可以看到執(zhí)行更新主要有以下兩個(gè)方法:

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

方法中都調(diào)用了CodePush.sync這個(gè)方法,我們看一下這個(gè)方法做了什么:

/**
     * Allows checking for an update, downloading it and installing it, all with a single call.
檢查更新、下載、安裝都在這個(gè)方法里啦啦啦啦啦。
     *
     * @param options Options used to configure the end-user update experience (e.g. show an prompt?, install the update immediately?).
  更新的參數(shù)
     * @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.
更新狀態(tài)改變的回調(diào)
     * @param downloadProgressCallback An optional callback that allows tracking the progress of an update while it is being downloaded.
下載進(jìn)度的回調(diào)
     * @param handleBinaryVersionMismatchCallback An optional callback for handling target binary version mismatch
版本不一致的回調(diào)(可選)
     */
    function sync(options?: SyncOptions,
       syncStatusChangedCallback?: SyncStatusChangedCallback, 
       downloadProgressCallback?: DowloadProgressCallback, 
       handleBinaryVersionMismatchCallback?: 
       HandleBinaryVersionMismatchCallback): Promise<SyncStatus>;

檢查更新的參數(shù):

export interface SyncOptions {
    deploymentKey?: string;
    檢查更新的部署密鑰,就是我們在code-push服務(wù)器上創(chuàng)建app時(shí)生成的key,服務(wù)器根據(jù)此來判斷是檢查哪一個(gè)應(yīng)用。

    installMode?: CodePush.InstallMode;//安裝模式,默認(rèn)是下一次restart時(shí)安裝,包含:
    IMMEDIATE(安裝更新并立刻重啟應(yīng)用),
    ON_NEXT_RESTART(安裝更新,但不立馬重啟,直到下一次重新進(jìn)入),
    ON_NEXT_RESUME(安裝更新,但是不立馬重新啟動,直到下一次從后臺恢復(fù)到前臺)
    ON_NEXT_SUSPEND(下一次處于后臺時(shí))
  
    mandatoryInstallMode?: CodePush.InstallMode;
    指定如果更新的版本是標(biāo)識了強(qiáng)制更新怎么更新。具體參數(shù)和上面一樣。

    minimumBackgroundDuration?: number;
    指定應(yīng)用程序需要在后臺重新啟動應(yīng)用程序之前所需的最小秒數(shù),默認(rèn)是0。

    updateDialog?: UpdateDialog;
    標(biāo)記檢查到更新是要彈出對話框
}

讓我們再看看CodePush中sync方法具體做了什么處理(代碼在node_modules/react-native-code-push/CodePush.js,本地的module代碼對應(yīng)的是react-native-code-push對應(yīng)的lib庫中的com/microsoft/codepush/react/CodePushNativeModule.java):

const sync = (() => {
  let syncInProgress = false;
  const setSyncCompleted = () => { syncInProgress = false; };
  //上面是互斥處理,保證多次調(diào)用sync只有一個(gè)在檢查更新,其他直接返回正在檢查中的狀態(tài)
  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;
   //調(diào)用了syncInternal方法
    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackkWithTryCatch, handleBinaryVersionMismatchCallback);
    syncPromise
      .then(setSyncCompleted)
      .catch(setSyncCompleted);

    return syncPromise;
  };
})();

我們再看一下syncInternal方法做了什么,這個(gè)里面就包含了所有的流程:

/*
該方法提供了一個(gè)簡單的在線合并檢查、下載、安裝更新
 * The syncInternal method provides a simple, one-line experience for
 * incorporating the check, download and installation of an update.
 *
它將這一些方法組合到一起,并支持強(qiáng)制更新、忽略失敗更新、更新確認(rèn)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;
  //準(zhǔn)備檢查更新的參數(shù)
  const syncOptions = {
    deploymentKey: null,//這個(gè)地方為null,是因?yàn)閺脑a中獲取的
    ignoreFailedUpdates: true,//是否忽略失敗的更新版本
    installMode: CodePush.InstallMode.ON_NEXT_RESTART,//安裝模式
    mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,//強(qiáng)制更新時(shí)的安裝模式
    minimumBackgroundDuration: 0,//重啟最小等待時(shí)間
    updateDialog: null,//是否要對話框
    ...options//其他傳遞過來的參數(shù)
  };
//更新狀態(tài)同步的回調(diào)
  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已經(jīng)ready。
    await CodePush.notifyApplicationReady();
   //發(fā)出第一個(gè)同步狀態(tài)。
    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) {
     //如果沒有更新或者當(dāng)前更新應(yīng)該被忽略的

      const currentPackage = await CodePush.getCurrentPackage();
      if (currentPackage && currentPackage.isPending) {
      //已經(jīng)安裝了            syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
        return CodePush.SyncStatus.UPDATE_INSTALLED;
      } else {
     //沒有可更新的內(nèi)容   
      syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
        return CodePush.SyncStatus.UP_TO_DATE;
      }
    } else if (syncOptions.updateDialog) {
//如果需要對話框,則需要用戶手動點(diǎn)擊下載安裝
     ...

      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方法是調(diào)用了notifyApplicationReadyInternal方法,而notifyApplicationReadyInternal方法如下:

async function notifyApplicationReadyInternal() {
  await NativeCodePush.notifyApplicationReady();//原生Module的方法
//獲取本地的狀態(tài),并上傳到服務(wù)器
  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的方法就是根據(jù)上一次檢查的狀態(tài)返回不同的值,此處不做細(xì)述。
tryReportStatus方法如下:

async function tryReportStatus(statusReport, resumeListener) {
    log(`statusReport =  (${statusReport})`);
  const config = await getConfiguration();//獲取配置信息
  const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;//當(dāng)前版本的標(biāo)簽或者app的版本
  const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;//部署的key
  try {
    if (statusReport.appVersion) {

      const sdk = getPromisifiedSdk(requestFetchAdapter, config);//得到操作的sdk,具體方法在前一個(gè)目錄下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);//本地保存狀態(tài)報(bào)告
    resumeListener && AppState.removeEventListener("change", resumeListener);//移除對于change時(shí)間的監(jiān)聽
  } catch (e) {
    log(`Report status failed: ${JSON.stringify(statusReport)}`);
    NativeCodePush.saveStatusReportForRetry(statusReport);//本地保存狀態(tài)報(bào)告并在resume時(shí)重新調(diào)用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

狀態(tài)什么的處理完后就開始執(zhí)行checkForUpdate檢查更新了。

async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
  /*
   //獲取本地配置信息,最終調(diào)用NativeCodePush.getConfiguration方法
   */
  const nativeConfig = await getConfiguration();

  const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
  const sdk = getPromisifiedSdk(requestFetchAdapter, config);//和服務(wù)器交互的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;
    }
  }
  //從服務(wù)器檢查更新
  const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);

  /*
   * 有四種結(jié)果:
   * ----------------------------------------------------------------
   * 1) 沒有更新.
   * 2) 服務(wù)器又更新,但是當(dāng)前原生版本不支持此版本
  
   * 3) 服務(wù)器告訴又更新,但是hsah值和本地是一樣的(理論上不會出現(xiàn))
   * 4) 服務(wù)器有更新,但是hash值和當(dāng)前運(yùn)行的二進(jìn)制(差異化的二進(jìn)制hash)是一樣的,這只會發(fā)生在Android手機(jī),我理解的是iOS沒有返回差異化的二進(jìn)制。
   */
  if (!update || update.updateAppVersion ||
      localPackage && (update.packageHash === localPackage.packageHash) ||
      (!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
    if (update && update.updateAppVersion) {
     //有更新,但是當(dāng)前原生的版本不兼容
      if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === "function") {
        handleBinaryVersionMismatchCallback(update)
      }
    }

    return null;
  } else {
//和本地對比,返回此更新的信息
    const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
    remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);//根據(jù)此hash判斷是否是失敗的更新(本地會存儲之前更新的狀態(tài))
    remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
    return remotePackage;
  }
}

需要有兩點(diǎn)需要注意:

1、remotePackage不僅僅有此版本的信息,同時(shí)也將PackageMixins的屬性包含進(jìn)來了,而PackageMixins對應(yīng)的是package-mixins.js的代碼,里面只有兩個(gè)屬性download下載更新和install安裝更新。里面的邏輯在講到下載時(shí)再細(xì)說。

2、上面提到的和服務(wù)器交互的sdkcode-push/script/acquisition-sdk.js里面有queryUpdateWithCurrentPackage查詢更新,reportStatusDeploy記錄狀態(tài)報(bào)告,reportStatusDownload下載狀態(tài)報(bào)告,這幾個(gè)方法,具體細(xì)節(jié)可以看源代碼,不復(fù)雜。

到目前為止,我們已經(jīng)得到了更新的版本信息。往下執(zhí)行會根據(jù)此版本信息來判斷是否是忽略版本,需要下載。如果需要下載,則會執(zhí)行前面提到的doDownloadAndInstall方法。

doDownloadAndInstall

doDownloadAndInstall里面代碼邏輯很少,如下:

const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);//同步狀態(tài)
      const localPackage = await remotePackage.download(downloadProgressCallback);//下載

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

      syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
//準(zhǔn)備安裝
      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;//下載進(jìn)度回調(diào)
        if (downloadProgressCallback) {
          const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
          // CodePushDownloadProgress該事件會有原生下載時(shí)發(fā)出
          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]);
//調(diào)用本地下載
          const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
//記錄下載狀態(tài)
          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下載并監(jiān)聽進(jìn)度回調(diào),具mUpdateManager的方法和普通下載一樣,不多細(xì)述。           
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;
                                        }
                                    });
                                }
                            });
                        }
//重點(diǎn)是該方法,通過時(shí)間不斷地發(fā)出下載的進(jìn)度狀態(tài)
                        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);
    }

通過上面源碼分析一下就可以知道下載流程,還是比較簡單的,重點(diǎn)說一下通知下載進(jìn)度給ReactNative的Component是通過getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());方法,而時(shí)間的event就是CodePushDownloadProgress,這個(gè)上面已經(jīng)提到過了。
下載完成之后就執(zhí)行到

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

      syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
//準(zhǔn)備安裝
      await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
        syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
      });

方法了,我們再來看一下具體的安裝方法源碼:

const local = {
//傳入?yún)?shù) 
installMode安裝模式,默認(rèn)NativeCodePush.codePushInstallModeOnNextRestart,
minimumBackgroundDuration上面提到的,如果是onresume的安裝模式,應(yīng)用從上一次暫停到前臺的時(shí)間間隔開始安裝,默認(rèn)為0。
updateInstalledCallback安裝回調(diào)。
    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
//調(diào)用本地的installUpdate方法,就是講bundle文件準(zhǔn)備好,以便重新加載,后面細(xì)述。
      await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
      updateInstalledCallback && updateInstalledCallback();
      if (installMode == NativeCodePush.codePushInstallModeImmediate) {
//如果安裝模式是立即安裝的話,直接重啟
        RestartManager.restartApp(false);
      } else {
//如果不是,清除已經(jīng)準(zhǔn)備好的重啟任務(wù)
        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中的安裝方法,里面有兩個(gè)點(diǎn)需要細(xì)述:

一、本地的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);
    }

這個(gè)里面有兩個(gè)知識點(diǎn):
1、是調(diào)用mUpdateManager.installPackage來處理已經(jīng)下載好的bundle文件,里面原理比較簡單,可以自行看一下。每一個(gè)版本的文件命名都是以其hash值命名的。
2、建立一個(gè)監(jiān)聽,根據(jù)安裝模式來判斷是在onHostResume中調(diào)用loadBundle還是在onHostPause中調(diào)用loadBundle

二、RestartManager這個(gè)類:
RestartManager這個(gè)類的源碼在RestartManager.js中,里面有兩個(gè)要素,一個(gè)是重啟任務(wù)的數(shù)組,這個(gè)不用細(xì)說;一個(gè)是重啟的方法,也是調(diào)用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);
    }

我們看到了里面關(guān)鍵的方法是loadBundle,這個(gè)方法獲取了一個(gè)ReactInstanceManager對象和最后更新下來的bundle文件路徑latestJSBundleFile,然后調(diào)用setJSBundle(instanceManager, latestJSBundleFile)方法完成重啟,這個(gè)方法源碼如下:

private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile) throws IllegalAccessException {
        try {
            JSBundleLoader latestJSBundleLoader;
//準(zhǔn)備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中重新設(shè)置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文件的重加載,具體的源碼就不多分析,后續(xù)有時(shí)間可以梳理一下ReactNative如何加載bundle文件的文章。重新加載完成之后,通過調(diào)用instanceManager.recreateReactContextInBackground();重新創(chuàng)建了ReactContext。

至此,一次完整的檢查--下載--安裝--更新流程完成了。

需要注意以下幾個(gè)知識點(diǎn):
1、ReactNative項(xiàng)目在進(jìn)入后臺是會發(fā)出background事件通知,在進(jìn)入前臺時(shí)會發(fā)出active事件通知。
2、熱更新檢查可以通過手動調(diào)用sync方法也可以在主界面的componentDidMount中通過事件觸發(fā)調(diào)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容