CodePush 資源更新原理及資源自動回滾的 bug 解析

1. 使用者使用如下方法調用 CodePush 的 sync 方法

RnCachePage.js

import React, {Component} from 'react';
import {
    View,
} from 'react-native';

import codePush from "react-native-code-push"; // 引入code-push

//配置 code push 更新頻率
let codePushOptions = {checkFrequency: codePush.CheckFrequency.ON_APP_RESUME};


class RnCachePageComponent extends Component {
    constructor(props) {
        super(props);
        this._codePushSync();
    }

    /**
     * 調用 syn 同步 code push
     */
    _codePushSync() {
        //https://github.com/microsoft/react-native-code-push/blob/master/docs/api-js.md
        //codePush.sync(options: Object, syncStatusChangeCallback: function(syncStatus: Number), downloadProgressCallback: function(progress: DownloadProgress), handleBinaryVersionMismatchCallback: function(update: RemotePackage)): Promise<Number>;
        codePush.sync({
            installMode: codePush.InstallMode.ON_NEXT_RESTART,
            mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
        }, (status) => {
            switch (status) {
                case codePush.SyncStatus.CHECKING_FOR_UPDATE:
                    console.log("Checking for updates.");
                    break;
                case codePush.SyncStatus.DOWNLOADING_PACKAGE:
                    console.log("Downloading package.");
                    break;
                case codePush.SyncStatus.INSTALLING_UPDATE:
                    console.log("Installing update.");
                    break;
                case codePush.SyncStatus.UP_TO_DATE:
                    console.log("Up-to-date.");
                    break;
                case codePush.SyncStatus.UPDATE_INSTALLED:
                    console.log("Update installed.");
                    break;
            }
        });
    }

    render() {
        return (
            <View style={{flex: 1, backgroundColor: 'white'}}>
            </View>
        )
    }
}

//codePush(rootComponent: React.Component): React.Component;
//codePush(options: CodePushOptions)(rootComponent: React.Component): React.Component;
const RnCachePage = codePush(codePushOptions)(RnCachePageComponent);

export default RnCachePage

2. CodePush.js sync 方法裏面通過調用 syncInternal 方法去檢查是否有資源更新

sync 核心代碼

    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);

sync 方法全部源碼

// This function allows only one syncInternal operation to proceed at any given time.
// Parallel calls to sync() while one is ongoing yields CodePush.SyncStatus.SYNC_IN_PROGRESS.
const sync = (() => {
  let syncInProgress = false;
  const setSyncCompleted = () => { syncInProgress = false; };

  return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
    let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
    if (typeof syncStatusChangeCallback === "function") {
      syncStatusCallbackWithTryCatch = (...args) => {
        try {
          syncStatusChangeCallback(...args);
        } catch (error) {
          log(`An error has occurred : ${error.stack}`);
        }
      }
    }

    if (typeof downloadProgressCallback === "function") {
      downloadProgressCallbackWithTryCatch = (...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;
    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
    syncPromise
      .then(setSyncCompleted)
      .catch(setSyncCompleted);

    return syncPromise;
  };
})();

3. CodePush.js 的 syncInternal 方法通過調用 checkForUpdate 去檢查資源是否有更新

核心代碼

    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

大部分源碼

async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
  let resolvedInstallMode;
  const syncOptions = {
    deploymentKey: null,
    ignoreFailedUpdates: true,
    rollbackRetryOptions: null,
    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:
            log("Checking for update.");
            break;
          case CodePush.SyncStatus.AWAITING_USER_ACTION:
            log("Awaiting user action.");
            break;
          case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
            log("Downloading package.");
            break;
          case CodePush.SyncStatus.INSTALLING_UPDATE:
            log("Installing update.");
            break;
          case CodePush.SyncStatus.UP_TO_DATE:
            log("App is up to date.");
            break;
          case CodePush.SyncStatus.UPDATE_IGNORED:
            log("User cancelled the update.");
            break;
          case CodePush.SyncStatus.UPDATE_INSTALLED:
            if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
              log("Update is installed and will be run on the next app restart.");
            } else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
              if (syncOptions.minimumBackgroundDuration > 0) {
                log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
              } else {
                log("Update is installed and will be run when the app next resumes.");
              }
            }
            break;
          case CodePush.SyncStatus.UNKNOWN_ERROR:
            log("An unknown error occurred.");
            break;
        }
      };

  try {
    await CodePush.notifyApplicationReady();

    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

    const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
      const localPackage = await remotePackage.download(downloadProgressCallback);
      ................
          } else {
      return await doDownloadAndInstall();
    }
  } catch (error) {
    syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
    log(error.message);
    throw error;
  }
};

注意,這裏頁其實會先去調用一下 notifyApplicationReady。如下:

await CodePush.notifyApplicationReady();
@ReactMethod
public void notifyApplicationReady(Promise promise) {
    try {
        mSettingsManager.removePendingUpdate();
        promise.resolve("");
    } catch(CodePushUnknownException e) {
        CodePushUtils.log(e);
        promise.reject(e);
    }
}

4. CodePush.js 的 checkForUpdate 方法會通過 PackageMixins 對象的 remote 方法去獲取一個『定義了 download 函數的新對象』。

PackageMixins 對象是什麼呢?其實就是引用的 package-mixins.js 文件。

核心代碼如下

//導入原生對象 NativeModules.CodePush ,即對應 java 中的 CodePushNativeModule.java 
let NativeCodePush = require("react-native").NativeModules.CodePush;
//導入 package-mixins.js 文件,並將原生的交互對象 NativeCodePush 傳遞到 package-mixins.js 文件中
const PackageMixins = require("./package-mixins")(NativeCodePush);
//調用 PackageMixins 的 remote 方法去獲取 remote 方法的返回值
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };

全部源碼如下

async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
  /*
   * Before we ask the server if an update exists, we
   * need to retrieve three pieces of information from the
   * native side: deployment key, app version (e.g. 1.0.1)
   * and the hash of the currently running update (if there is one).
   * This allows the client to only receive updates which are targetted
   * for their specific deployment and version and which are actually
   * different from the CodePush update they have already installed.
   */
  const nativeConfig = await getConfiguration();
  /*
   * If a deployment key was explicitly provided,
   * then let's override the one we retrieved
   * from the native-side of the app. This allows
   * dynamically "redirecting" end-users at different
   * deployments (e.g. an early access deployment for insiders).
   */
  const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
  const sdk = getPromisifiedSdk(requestFetchAdapter, config);

  // Use dynamically overridden getCurrentPackage() during tests.
  const localPackage = await module.exports.getCurrentPackage();

  /*
   * If the app has a previously installed update, and that update
   * was targetted at the same app version that is currently running,
   * then we want to use its package hash to determine whether a new
   * release has been made on the server. Otherwise, we only need
   * to send the app version to the server, since we are interested
   * in any updates for current app store version, regardless of hash.
   */
  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);

  /*
   * There are four cases where checkForUpdate will resolve to null:
   * ----------------------------------------------------------------
   * 1) The server said there isn't an update. This is the most common case.
   * 2) The server said there is an update but it requires a newer binary version.
   *    This would occur when end-users are running an older app store version than
   *    is available, and CodePush is making sure they don't get an update that
   *    potentially wouldn't be compatible with what they are running.
   * 3) The server said there is an update, but the update's hash is the same as
   *    the currently running update. This should _never_ happen, unless there is a
   *    bug in the server, but we're adding this check just to double-check that the
   *    client app is resilient to a potential issue with the update check.
   * 4) The server said there is an update, but the update's hash is the same as that
   *    of the binary's currently running version. This should only happen in Android -
   *    unlike iOS, we don't attach the binary's hash to the updateCheck request
   *    because we want to avoid having to install diff updates against the binary's
   *    version, which we can't do yet on Android.
   */
  if (!update || update.updateAppVersion ||
      localPackage && (update.packageHash === localPackage.packageHash) ||
      (!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
    if (update && update.updateAppVersion) {
      log("An update is available but it is not targeting the binary version of your app.");
      if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === "function") {
        handleBinaryVersionMismatchCallback(update)
      }
    }

    return null;
  } else {
    const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
    remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
    remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
    return remotePackage;
  }
}

補充 CodePushNativeModule.java 相關源碼。就是這個原生模塊在一直與 js 進行交互。最終實現資源的更新。

//繼承 ReactContextBaseJavaModule
public class CodePushNativeModule extends ReactContextBaseJavaModule {
// module 名爲 『CodePush』
@Override
public String getName() {
    return "CodePush";
}

5. package-mixins.js 的 remote 方法會新生成一個 js 對象並返回。在這個 js 對象中,定義了一個叫做『async download(downloadProgressCallback)』的函數

核心代碼: remote 方法的定義與返回值

  const remote = (reportStatusDownload) => {
    return {
      async download(downloadProgressCallback) {
      ...... 省略 ......
      },

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

remote 全部源碼

  const remote = (reportStatusDownload) => {
    return {
      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);
          // Use event subscription to obtain download progress.
          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);

          if (reportStatusDownload) {
            reportStatusDownload(this)
            .catch((err) => {
              log(`Report download status failed: ${err}`);
            });
          }

          return { ...downloadedPackage, ...local };
        } finally {
          downloadProgressSubscription && downloadProgressSubscription.remove();
        }
      },

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

6. 上面說的是誰調用的 checkForUpdate 方法?如果還記得的話(不記得,自己返回去看),就知道是 CodePush.js 的 syncInternal 方法。

相關核心代碼如下。下面的 const doDownloadAndInstall 需要重點關注下。因爲就是這個方法開始負責與原生交互,實現真正的資源下載與安裝。

//定義 doDownloadAndInstall 方法
const doDownloadAndInstall = async () => {
  syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
  //執行 remotePackage 的 download 方法
  const localPackage = await remotePackage.download(downloadProgressCallback);

  // Determine the correct install mode based on whether the update is mandatory or not.
  resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;

  syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
  //****** 執行 localPackage 的 install 方法(也就是上面說的 local 對象裏面定義了一個 install 方法)  ******
  await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
    syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
  });

  return CodePush.SyncStatus.UPDATE_INSTALLED;
};

//忽略更新
if (!remotePackage || updateShouldBeIgnored) {
  if (updateShouldBeIgnored) {
      log("An update is available, but it is being ignored due to having been previously rolled back.");
  }

  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) {
//顯示一個彈出框,讓用戶選擇是否更新
  // updateDialog supports any truthy value (e.g. true, "goo", 12),
  // but we should treat a non-object value as just the default dialog
  if (typeof syncOptions.updateDialog !== "object") {
    syncOptions.updateDialog = CodePush.DEFAULT_UPDATE_DIALOG;
   } else {
        syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
      }

      return await new Promise((resolve, reject) => {
        let message = null;
        let installButtonText = null;

        const 

    ...... 省略 ......

    //其它情況
    } else {
        // ***注意:開始更新了****
      return await doDownloadAndInstall();
    }
  } catch (error) {
    syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
    log(error.message);
    throw error;
  }

如上,其它情況下,return await doDownloadAndInstall(); 將觸發 doDownloadAndInstall 方法。最終調用到原生代碼中。

7. package-mixins.js 的 『remote 方法返回的 js 對象』的 『download 方法』通過『 NativeCodePush(原生模塊) 的 downloadUpdate 方法』,最終回調到 java 原生中,實現資源的下載。

注意,const downloadedPackage 變量裏面保存了原生 NativeCodePush.downloadUpdate 方法返回的 JSON 對象。

核心代碼

const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);

remote 全部源碼

  const remote = (reportStatusDownload) => {
    return {
      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);
          // Use event subscription to obtain download progress.
          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);

          if (reportStatusDownload) {
            reportStatusDownload(this)
            .catch((err) => {
              log(`Report download status failed: ${err}`);
            });
          }

          return { ...downloadedPackage, ...local };
        } finally {
          downloadProgressSubscription && downloadProgressSubscription.remove();
        }
      },

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

8. CodePushNativeModule.java 的 downloadUpdate 方法通過 CodePushUpdateManager.java 類的 downloadPackage 方法實現資源包的下載邏輯

1. 通過 AsyncTask 實現線程切換,在子線程中下載資源文件

2. CodePushUpdateManager 的 downloadPackage 在當前線程中通過 URL.openConnection(即 HttpURLConnection )實現資源下載。

3. 也就是CodePushUpdateManager.java 的 downloadPackage 方法裏面沒有線程切換。CodePushNativeModule.java 的 downloadUpdate 方法通過 AsyncTask 實現線程切換。

    @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.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 (CodePushInvalidUpdateException e) {
                    CodePushUtils.log(e);
                    mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage));
                    promise.reject(e);
                } catch (IOException | CodePushUnknownException e) {
                    CodePushUtils.log(e);
                    promise.reject(e);
                }

                return null;
            }
        };

        asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

9. 下載完成後,再回到 CodePushNativeModule.java 的 downloadUpdate 方法。上面資源下載完成後會調用 promise.resolve 方法返回到 js 端。

JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));

下面通過 tryGetString 獲取 packageHash 對應的值

//方法的形參列表裏面有一個 ReadableMap 類型的 updatePackage。這個其實是 js 端調用 downloadUpdate 方法的時候傳遞過來的
public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {

//tryGetString 是獲取 js 傳遞過來的參數中的 packageHash 值
public static final String PACKAGE_HASH_KEY = "packageHash";
CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY

下面利用 packageHash 去獲取 app.json 中對應的信息(即上面調用的 mUpdateManager.getPackage 方法)

public static final String PACKAGE_FILE_NAME = "app.json";

//下面是 CodePushUpdateManager.java 的  getPackage 方法
public JSONObject getPackage(String packageHash) {
    String folderPath = getPackageFolderPath(packageHash);
    String packageFilePath = CodePushUtils.appendPathComponent(folderPath, CodePushConstants.PACKAGE_FILE_NAME);
    try {
        return CodePushUtils.getJsonObjectFromFile(packageFilePath);
    } catch (IOException e) {
        return null;
    }
}

下面通過 CodePushUtils 將 JSONObject 轉換到 react native 支持的 WritableMap 類型,並通過 promise 返回給 js 端。

promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));

10. 如上,調用 promise.resolve 之後,將再次回到 package-mixins.js 的 download 方法。(download 方法定義在了 remote 方法中)

核心代碼如下

const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);

if (reportStatusDownload) {
reportStatusDownload(this)
.catch((err) => {
  log(`Report download status failed: ${err}`);
});
}

return { ...downloadedPackage, ...local };

上面原生代碼返回的 JSONObject 對象,被 const downloadedPackage 臨時變量接收。
最後,這個原生返回的 JSONObject 會和 local 對象一起 return 回去。

local 對象是什麼?

package-mixins.js 裏面定義了 local 對象。注意,他有一個 install 方法。(**重點:** local 對象有一個 install 方法)

  const local = {
    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
      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
  };

注意到上面的 ...local。意思是將 local 對象上的 install 方法定義取出來,打包到一個空對象中(return {新對象就是我了} ),最後再返回這個新創建的對象。

11. 好了,資源下載完了, local 對象也返回了,下一步就又回到了 CodePush.js 的 checkForUpdate 方法中。

核心代碼如下。最終又將這個 local 對象 return 出去了。

async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
......
    const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
    //這裏爲 remotePackage 添加了 failedInstall 成員變量
    remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
    remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
    //這裏將 remotePackage 又返回出去了
    return remotePackage;
  }
}

注意,isFailedUpdate 其實是原生方法。對應 CodePushNativeModule.java 中的 isFailedUpdate 方法。 

isFailedUpdate 的作用是調用 SettingsManager 的 isFailedHash 方法,判斷這個資源包是否下載失敗。

原生 isFailedUpdate 的返回值,將保存在 remotePackage 中。並在 checkForUpdate 方法中被 return 出去了。

@ReactMethod
public void isFailedUpdate(String packageHash, Promise promise) {
    try {
        promise.resolve(mSettingsManager.isFailedHash(packageHash));
    } catch (CodePushUnknownException e) {
        CodePushUtils.log(e);
        promise.reject(e);
    }
}

SettingsManager.java 的 saveFailedUpdate 方法保存哪些資源包下載失敗

    public void saveFailedUpdate(JSONObject failedPackage) {
        try {
            if (isFailedHash(failedPackage.getString(CodePushConstants.PACKAGE_HASH_KEY))) {
                // Do not need to add the package if it is already in the failedUpdates.
                return;
            }
        } catch (JSONException e) {
            throw new CodePushUnknownException("Unable to read package hash from package.", e);
        }

        String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null);
        JSONArray failedUpdates;
        if (failedUpdatesString == null) {
            failedUpdates = new JSONArray();
        } else {
            try {
                failedUpdates = new JSONArray(failedUpdatesString);
            } catch (JSONException e) {
                // Should not happen.
                throw new CodePushMalformedDataException("Unable to parse failed updates information " +
                        failedUpdatesString + " stored in SharedPreferences", e);
            }
        }

        failedUpdates.put(failedPackage);
        mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, failedUpdates.toString()).commit();
    }

SettingsManager.java 的 getFailedUpdates 方法獲取哪些資源包下載失敗

    public JSONArray getFailedUpdates() {
        String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null);
        if (failedUpdatesString == null) {
            return new JSONArray();
        }

        try {
            return new JSONArray(failedUpdatesString);
        } catch (JSONException e) {
            // Unrecognized data format, clear and replace with expected format.
            JSONArray emptyArray = new JSONArray();
            mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, emptyArray.toString()).commit();
            return emptyArray;
        }
    }

SettingsManager.java 的 isFailedHash 方法判斷指定資源包是否下載失敗

    public boolean isFailedHash(String packageHash) {
        JSONArray failedUpdates = getFailedUpdates();
        if (packageHash != null) {
            for (int i = 0; i < failedUpdates.length(); i++) {
                try {
                    JSONObject failedPackage = failedUpdates.getJSONObject(i);
                    String failedPackageHash = failedPackage.getString(CodePushConstants.PACKAGE_HASH_KEY);
                    if (packageHash.equals(failedPackageHash)) {
                        return true;
                    }
                } catch (JSONException e) {
                    throw new CodePushUnknownException("Unable to read failedUpdates data stored in SharedPreferences.", e);
                }
            }
        }

        return false;
    }

12. 如上,還記得是誰調用的 checkForUpdate 方法嗎?答案是 CodePush.js 的 syncInternal 方法。

如上,在調用完 remotePackage.download 方法後,獲取到了一個 download 方法的返回對象。這個返回對象裏面有一個 install 方法。下面通過調用 install 方法,最終回調到原生將『剛剛下載的最新資源』加載到用戶面前。

async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
  let resolvedInstallMode;
    ......
    try {
    await CodePush.notifyApplicationReady();

    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
    //就是這裏調用了 checkForUpdate 方法(返回的 remotePackage 對象裏面有一個 download 方法。作用參考第『4』大點。)
    //4. CodePush.js 的 checkForUpdate 方法會通過 PackageMixins 對象的 remote 方法去獲取一個『定義了 download 函數的新對象』。
    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
    ......
     const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
      //下載資源
      //(記住,返回的 localPackage 對象裏面有一個叫做 local 對象的東西。local 對象裏面定義了 install 方法)
      const localPackage = await remotePackage.download(downloadProgressCallback);

      // Determine the correct install mode based on whether the update is mandatory or not.
      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;
    };

install 方法是誰定義的?(不記得的還是可以回去看上面)

package-mixins.js 裏面定義了 install 方法

  const local = {
    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
      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
  };

13. package-mixins.js 的 install 方法通過 NativeCodePush.installUpdate 安裝最新下載的資源

//通過原生,安裝最新資源
await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);

14. CodePushNativeModule.java 的 installUpdate 方法通過 mUpdateManager.installPackage 安裝最新下載的資源

CodePushNativeModule.java installUpdate 核心源碼

//安裝最新資源
mUpdateManager.installPackage(CodePushUtils.convertReadableToJsonObject(updatePackage), mSettingsManager.isPendingUpdate(null));

//添加一個標記位。標識當前『正在更新』的狀態。
String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY);
mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false);

//監聽 Activity 生命週期
// Ensure we do not add the listener twice.
mLifecycleEventListener = new LifecycleEventListener() {
    private Date lastPausedDate = null;
    private Handler appSuspendHandler = new Handler(Looper.getMainLooper());
    //定義一個 Runnable ,裏面有 loadBundle 方法
    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();
            }
        }
    }
    ......
};

//添加對全局 Activity 生命週期的監聽
getReactApplicationContext().addLifecycleEventListener(mLifecycleEventListener);
//通知 js 端,原生已經將資源安裝好。並且加載好了。
promise.resolve("");

CodePushUpdateManager.java 的 installPackage 方法

    public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) {
        String packageHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
        JSONObject info = getCurrentPackageInfo();

        String currentPackageHash = info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null);
        if (packageHash != null && packageHash.equals(currentPackageHash)) {
            // The current package is already the one being installed, so we should no-op.
            return;
        }

        if (removePendingUpdate) {
            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));
        }

        CodePushUtils.setJSONValueForKey(info, CodePushConstants.CURRENT_PACKAGE_KEY, packageHash);
        updateCurrentPackageInfo(info);
    }

CodePushNativeModule.java 的 loadBundle 方法

    private void loadBundle() {
        clearLifecycleEventListener();
        try {
            mCodePush.clearDebugCacheIfNeeded(resolveInstanceManager());
        } catch(Exception e) {
            // If we got error in out reflection we should clear debug cache anyway.
            mCodePush.clearDebugCacheIfNeeded(null);
        }

        try {
            // #1) Get the ReactInstanceManager instance, which is what includes the
            //     logic to reload the current React context.
            final ReactInstanceManager instanceManager = resolveInstanceManager();
            if (instanceManager == null) {
                return;
            }

            String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());

            // #2) Update the locally stored JS bundle file path
            setJSBundle(instanceManager, latestJSBundleFile);

            // #3) Get the context creation method and fire it on the UI thread (which RN enforces)
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    try {
                        // We don't need to resetReactRootViews anymore 
                        // due the issue https://github.com/facebook/react-native/issues/14533
                        // has been fixed in RN 0.46.0
                        //resetReactRootViews(instanceManager);

                        instanceManager.recreateReactContextInBackground();
                        mCodePush.initializeUpdateAfterRestart();
                    } catch (Exception e) {
                        // The recreation method threw an unknown exception
                        // so just simply fallback to restarting the Activity (if it exists)
                        loadBundleLegacy();
                    }
                }
            });

        } catch (Exception e) {
            // Our reflection logic failed somewhere
            // so fall back to restarting the Activity (if it exists)
            CodePushUtils.log("Failed to load the bundle, falling back to restarting the Activity (if it exists). " + e.getMessage());
            loadBundleLegacy();
        }
    }

15. 原生的 installUpdate 返回後,package-mixins.js 中的 install 方法又將繼續調用 RestartManager.restartApp 方法。

注意,如果是 installMode 爲 IMMEDIATE ,則直接調用 RestartManager.restartApp 

否則調用 RestartManager.clearPendingRestart 方法。

  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
  }
import RestartManager from "./RestartManager";

下面是 CodePushNativeModule.java 注入到 js 中的一些常量。

@Override
public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();

    constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue());
    constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue());
    constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue());
    constants.put("codePushInstallModeOnNextSuspend", CodePushInstallMode.ON_NEXT_SUSPEND.getValue());

    constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue());
    constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue());
    constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue());

    return constants;
}

16. RestartManager.js 中的 restartApp 方法通過 NativeCodePush.restartApp 回調到原生,通過 recreateReactContextInBackground 重建 react native 上下文對象,實現新資源的生效。

const log = require("./logging");
const NativeCodePush = require("react-native").NativeModules.CodePush;

const RestartManager = (() => {
    let _allowed = true;
    let _restartInProgress = false;
    let _restartQueue = [];

    function allow() {
        log("Re-allowing restarts");
        _allowed = true;

        if (_restartQueue.length) {
            log("Executing pending restart");
            restartApp(_restartQueue.shift(1));
        }
    }

    function clearPendingRestart() {
        _restartQueue = [];
    }

    function disallow() {
        log("Disallowing restarts");
        _allowed = false;
    }

    async function restartApp(onlyIfUpdateIsPending = false) {
        if (_restartInProgress) {
            log("Restart request queued until the current restart is completed");
            _restartQueue.push(onlyIfUpdateIsPending);
        } else if (!_allowed) {
            log("Restart request queued until restarts are re-allowed");
            _restartQueue.push(onlyIfUpdateIsPending);
        } else {
            _restartInProgress = true;
            //這裏調用原生的 restartApp 方法實現新資源生效
            if (await NativeCodePush.restartApp(onlyIfUpdateIsPending)) {
                // The app has already restarted, so there is no need to
                // process the remaining queued restarts.
                log("Restarting app");
                return;
            }

            _restartInProgress = false;
            if (_restartQueue.length) {
                restartApp(_restartQueue.shift(1));
            }
        }
    }

    return {
        allow,
        clearPendingRestart,
        disallow,
        restartApp
    };
})();

module.exports = RestartManager;

17. CodePushNativeModule.java 的 restartApp 方法最終調用了 react native 的上下文切換的 api,實現最新資源的生效。

restartApp 方法

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

        promise.resolve(false);
    } catch(CodePushUnknownException e) {
        CodePushUtils.log(e);
        promise.reject(e);
    }
}

loadBundle 方法

    private void loadBundle() {
        clearLifecycleEventListener();
        try {
            mCodePush.clearDebugCacheIfNeeded(resolveInstanceManager());
        } catch(Exception e) {
            // If we got error in out reflection we should clear debug cache anyway.
            mCodePush.clearDebugCacheIfNeeded(null);
        }

        try {
            // #1) Get the ReactInstanceManager instance, which is what includes the
            //     logic to reload the current React context.
            final ReactInstanceManager instanceManager = resolveInstanceManager();
            if (instanceManager == null) {
                return;
            }

            String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());

            // #2) Update the locally stored JS bundle file path
            setJSBundle(instanceManager, latestJSBundleFile);

            // #3) Get the context creation method and fire it on the UI thread (which RN enforces)
            //這裏 new 了一個 Handler ,將線程切換到 UI 線程。再調用 recreateReactContextInBackground 方法,重新創建了 react native 的上下文。
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    try {
                        // We don't need to resetReactRootViews anymore 
                        // due the issue https://github.com/facebook/react-native/issues/14533
                        // has been fixed in RN 0.46.0
                        //resetReactRootViews(instanceManager);

                        //就是這句代碼,讓最新資源立即生效。
                        instanceManager.recreateReactContextInBackground();
                        mCodePush.initializeUpdateAfterRestart();
                    } catch (Exception e) {
                        // The recreation method threw an unknown exception
                        // so just simply fallback to restarting the Activity (if it exists)
                        loadBundleLegacy();
                    }
                }
            });

        } catch (Exception e) {
            // Our reflection logic failed somewhere
            // so fall back to restarting the Activity (if it exists)
            CodePushUtils.log("Failed to load the bundle, falling back to restarting the Activity (if it exists). " + e.getMessage());
            loadBundleLegacy();
        }
    }

recreateReactContextInBackground 方法。

當配置已經更新,通過此方法可以重建 react native 的上下文

  /**
   * Recreate the react application and context. This should be called if configuration has changed
   * or the developer has requested the app to be reloaded. It should only be called after an
   * initial call to createReactContextInBackground.
   *
   * <p>Called from UI thread.
   */
  @ThreadConfined(UI)
  public void recreateReactContextInBackground() {
    Assertions.assertCondition(
        mHasStartedCreatingInitialContext,
        "recreateReactContextInBackground should only be called after the initial " +
            "createReactContextInBackground call.");
    recreateReactContextInBackgroundInner();
  }

補充: CodePush 加載完最新資源包,還沒有更新完成的時候,就立即退出 rn 頁面。在沒有再次進入有 codePush 相關代碼的 rn 界面的情況下,就殺死應用退出程序。當你再次進入應用,打開 rn 容器時,資源包會自動回滾。自動回滾完成後,再也不會去更新服務端當前最新的資源包了。 —— bug

CodePush.js 的 syncInternal 方法

此方法將最終調用原生的 removePendingUpdate 方法

async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
  ......
  try {
    //此方法將最終調用原生的 removePendingUpdate 方法
    await CodePush.notifyApplicationReady();

    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

    const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
      const localPackage = await remotePackage.download(downloadProgressCallback);
      ......
};

Pending 標記位移除之後,將不會有自動回滾的問題

@ReactMethod
public void notifyApplicationReady(Promise promise) {
    try {
        //Pending 標記位移除之後,將不會有自動回滾的問題
        mSettingsManager.removePendingUpdate();
        promise.resolve("");
    } catch(CodePushUnknownException e) {
        CodePushUtils.log(e);
        promise.reject(e);
    }
}

結論:

(a. notifyApplicationReady 可以理解成是一個原生的刪除標記位的方法。他的調用位置在 CodePush.js 的 syncInternal 方法中。當然,notifyApplicationReady 的起始調用位置來自 RnCachePage.js 的組件構造器方法中。因爲在 RnCachePage.js - RnCachePageComponent 組件構造器方法中調用了 codePush.sync 方法。而這個方法,最終就會調到前面說的 syncInternal 方法裏面。

(b. 每次 react native 容器新啓動的時候,打開某個 rn 界面的時候,就會重新初始化 js 組件,也就會調到 RnCachePageComponent 的構造器方法中。

(c. 每次 react native 上下文重新創建的時候,就會重新初始化當面打開的 js 組件。

(d. 根據上面內容『16. RestartManager.js 中的 restartApp 方法通過 NativeCodePush.restartApp 回調到原生,通過 recreateReactContextInBackground 重建 react native 上下文對象,實現新資源的生效。』可知,如果服務端的資源有更新,最終會在 restartApp 的時候重建 react native 上下文。也就會觸發 js 組件的重建。

(e. 當服務端最新資源加載完成,觸發 js 組件重建的時候。如果剛好這個 js 組件的 『構造器中』或者 『componentDidMount 生命週期中』也調用了 codePush.sync 方法。那麼就會刪除 Pending 標記位。

注意,如下 5 步,即便你沒有主動調用 codePush.sync 方法,也可能會使用 codePush 包裹你的根組件,他同樣會間接調用 CodePush.sync 方法。

//*** 第 3 步:導入 hoistStatics 函數,用於靜態方法的複製。類似於 Object.assign 方法
import hoistStatics from 'hoist-non-react-statics';

......

function codePushify(options = {}) {
 ......
  //*** 第 2 步,decorator 裝飾器函數的定義。
  var decorator = (RootComponent) => {
    const extended = class CodePushComponent extends React.Component {
      componentDidMount() {
        if (options.checkFrequency === CodePush.CheckFrequency.MANUAL) {
          CodePush.notifyAppReady();
        } else {
          let rootComponentInstance = this.refs.rootComponent;

          let syncStatusCallback;
          if (rootComponentInstance && rootComponentInstance.codePushStatusDidChange) {
            syncStatusCallback = rootComponentInstance.codePushStatusDidChange;
            if (rootComponentInstance instanceof React.Component) {
              syncStatusCallback = syncStatusCallback.bind(rootComponentInstance);
            }
          }

          let downloadProgressCallback;

          //如下可知,使用 codePush 函數直接包裹 js 根組件的方式,可以通過重寫 codePushDownloadDidProgress 方法,實現對下資源下載進度的監聽

          //https://github.com/microsoft/react-native-code-push/blob/master/docs/api-js.md
          //Log/display progress. While the app is syncing with the server for updates, make use of the codePushStatusDidChange and/or codePushDownloadDidProgress event hooks to log down the different stages of this process, or even display a progress bar to the user.

          if (rootComponentInstance && rootComponentInstance.codePushDownloadDidProgress) {
            downloadProgressCallback = rootComponentInstance.codePushDownloadDidProgress;
            if (rootComponentInstance instanceof React.Component) {
              //爲 codePushDownloadDidProgress 生命週期方法綁定 this 對象
              downloadProgressCallback = downloadProgressCallback.bind(rootComponentInstance);
            }
          }

          let handleBinaryVersionMismatchCallback;
          if (rootComponentInstance && rootComponentInstance.codePushOnBinaryVersionMismatch) {
            handleBinaryVersionMismatchCallback = rootComponentInstance.codePushOnBinaryVersionMismatch;
            if (rootComponentInstance instanceof React.Component) {
              handleBinaryVersionMismatchCallback = handleBinaryVersionMismatchCallback.bind(rootComponentInstance);
            }
          }

          //第 5 步,最終還是調用的 CodePush.sync 方法。
          CodePush.sync(options, syncStatusCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback);
          if (options.checkFrequency === CodePush.CheckFrequency.ON_APP_RESUME) {
            ReactNative.AppState.addEventListener("change", (newState) => {
              newState === "active" && CodePush.sync(options, syncStatusCallback, downloadProgressCallback);
            });
          }
        }
      }

      render() {
        const props = {...this.props};

        // we can set ref property on class components only (not stateless)
        // check it by render method
        if (RootComponent.prototype.render) {
          props.ref = "rootComponent";
        }

        return <RootComponent {...props} />
      }
    }

    //*** 第 4 步,hoistStatics 是什麼作用,它實際上就是類似 Object.assign 將子組件中的 static 方法複製進父組件,但不會覆蓋組件中的關鍵字方法(如 componentDidMount)。
    return hoistStatics(extended, RootComponent);
  }

  if (typeof options === "function") {
    // Infer that the root component was directly passed to us.
    //如果用戶傳遞過來了一個 js 根組件,則會通過調用 decorator 方法,對調用都傳遞過來的 js 組件進行裝飾。
    //調用示例:const RnCachePage = codePush(RnCachePageComponent);
    return decorator(options);
  } else {
    //*** 第 1 步,如果用戶傳遞過來了一個 options 配置對象,則直接返回 decorator 裝飾器函數,然後什麼也不執行。(原因,這裏返回的裝飾器函數由於在閉包內,會引用用戶剛剛傳遞過來的 options 配置對象。如下,緊接着的一個 『(RnCachePageComponent)』小括號 則是執行了這個 decorator 裝飾器函數 )
    //調用示例:const RnCachePage = codePush(codePushOptions)(RnCachePageComponent);
    return decorator;
  }
}

hoist-non-react-statics

hoistStatics 方法可類比,Object.assign 方法,用於對象的合併,將源對象( source )的所有可枚舉屬性,複製到目標對象( target )。

由上面 5 步可知,即使 使用者不主動調用 CodePush.sync 方法,codePush 包裹了 js 根組件也會最終 CodePush.sync 方法。
而使用 codePush 包裹根組件又是必須要做的一步,當使用者又主動調用了 CodePush.sync 方法,是不是這個 CodePush.sync 方法會調用兩次呢?答案顯示是肯定的。 

CodePush.js 的 sync 方法中有一個 let syncInProgress = false;。當調用 CodePush.sync 後,會立即將 syncInProgress 設置爲 true。當第二次調用 CodePush.sync 時,會打印控制檯日誌 『Sync already in progress』

// This function allows only one syncInternal operation to proceed at any given time.
// Parallel calls to sync() while one is ongoing yields CodePush.SyncStatus.SYNC_IN_PROGRESS.
const sync = (() => {
  let syncInProgress = false;
  const setSyncCompleted = () => { syncInProgress = false; };

    ......

    if (syncInProgress) {
      //如果第 2 次調用時傳遞了資源狀態監聽函數,將通過 syncStatusCallbackWithTryCatch 函數,返回『CodePush.SyncStatus.SYNC_IN_PROGRESS』狀態。否則,將打印日誌『Sync already in progress』
      typeof syncStatusCallbackWithTryCatch === "function"
        ? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
        : log("Sync already in progress.");
      return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
    }

    syncInProgress = true;
    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
    syncPromise
      .then(setSyncCompleted)
      .catch(setSyncCompleted);

    return syncPromise;
  };
})();

(f. 如上,刪除了 Pending 標記位之後。CodePush.java 構造器中的 initializeUpdateAfterRestart() 就不會執行自動回滾。

CodePush.java 的構造方法

    public CodePush(String deploymentKey, Context context, boolean isDebugMode) {
        mContext = context.getApplicationContext();

        mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath());
        mTelemetryManager = new CodePushTelemetryManager(mContext);
        mDeploymentKey = deploymentKey;
        mIsDebugMode = isDebugMode;
        mSettingsManager = new SettingsManager(mContext);

        if (sAppVersion == null) {
            try {
                PackageInfo pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
                sAppVersion = pInfo.versionName;
            } catch (PackageManager.NameNotFoundException e) {
                throw new CodePushUnknownException("Unable to get package info for " + mContext.getPackageName(), e);
            }
        }

        mCurrentInstance = this;

        clearDebugCacheIfNeeded(null);
        initializeUpdateAfterRestart();
    }

下面是自動回滾代碼

    void initializeUpdateAfterRestart() {
        // Reset the state which indicates that
        // the app was just freshly updated.
        mDidUpdate = false;

        //*** mSettingsManager.getPendingUpdate ,檢查 Pending 標記位。如果有 Pending 標記位,就在控制檯打印一個 『Update did not finish loading the last time, rolling back to a previous version』的 Log ,並且進行自動回滾。
        JSONObject pendingUpdate = mSettingsManager.getPendingUpdate();
        if (pendingUpdate != null) {
            JSONObject packageMetadata = this.mUpdateManager.getCurrentPackage();
            if (packageMetadata == null || !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 update was initialized, but notifyApplicationReady was not called.
                    // Therefore, deduce that it is a broken update and rollback.
                    CodePushUtils.log("Update did not finish loading the last time, rolling back to a previous version.");
                    sNeedToReportRollback = true;
                    rollbackPackage();
                } else {
                    // There is in fact a new update running for the first
                    // time, so update the local state to ensure the client knows.
                    mDidUpdate = true;

                    // Mark that we tried to initialize the new update, so that if it crashes,
                    // we will know that we need to rollback when the app next starts.
                    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);
            }
        }
    }

(g. 關於上面四個結論,一個是 CodePush.sync 將觸發標記位刪除邏輯;二是,僅使用 codePush 函數包裹 js 根組件,最終也會調用到 CodePush.sync 方法;三是,只要最新的資源加載完成了,並且最新資源加載完成後在任意一個頁面觸發了一次 CodePush.sync 方法。將會成功刪除標記位;四是,如果標記位沒有刪除,將會在 CodePush.java 的構造器中觸發自動回滾。可以通過下面的方案進行驗證。

1. 將 RnCachePageComponent 組件使用 codePush 函數包裹。並在其構造器中打印日誌,如 『RnCachePageComponent 調用了 codePush.sync』

2. 將 VolunteerIndexTabPage 組件使用 codePush 函數包裹。並在其構造器中打印日誌,如 『VolunteerIndexTabPage 調用了 codePush.sync』

3. DailyQuestion 組件不使用 codePush 函數包裹。並在其構造器中打印日誌,如『DailyQuestion 沒有 codePush 相關代碼』

4. 打出一個 js bundle 包,我們暫時叫做 『local-bundle-001』。並將這個 bundle 放到 apk 中。在手機上安裝好這個 apk。

5. 修改 DailyQuestion 組件的代碼,在其構造器中彈出一個 Toast ,內容爲『DailyQuestion 頁。codePush 資源包更新成功』。並打出一個 js bundle 包,我們暫時叫做 『remote-bundle-002』。

6. 使用在線 codePush 發佈系統發佈這個 『remote-bundle-002』。

7. 打開你的 App。進入 RnCachePageComponent 頁。並且快速退出 RnCachePageComponent 頁。你 『會』在控制檯收到 『RnCachePageComponent 調用了 codePush.sync』 的日誌

測試路徑 1. 如下: ( codePush 自動回滾 bug 測試)

8. 過一陣子,殺死 App,重新進入 DailyQuestion 頁,你會在控制檯收到 code push 官方給你發出的一條自動回滾日誌。『[CodePush] Update did not finish loading the last time, rolling back to a previous version.』

注意,其實,如果你在 App 的 Application 的生命週期方法中初始化了 react native Application ,你殺死 App 之後,不重新進入 DailyQuestion 頁,只需要重新打開你的 App, 也會收到上面的 codePush 資源自動回滾的日誌。

當你收到上面的自動回滾的日誌時,說明自動回滾已經產生。 codePush 無法更新服務端最新資源的 bug 也就復現了。

測試路徑 2. 如下: ( 只要有任意一個被 codePush 包裹的 js 組件被打開,都可以刪除標記位)

8. 如果你做了測試路徑 1. 。你需要卸載你的 apk 再重新安裝。並重新執行步驟 7.『打開你的 App。進入 RnCachePageComponent 頁 ... 控制檯收到 『RnCachePageComponent 調用了 codePush.sync』 的日誌』

9. 過一陣子,進入 VolunteerIndexTabPage 頁,你會在控制檯收到『VolunteerIndexTabPage 調用了 codePush.sync』

10. 過一陣子,殺死 App,重新進入 DailyQuestion 頁,你 『不會』 在控制檯收到 code push 官方給你發出的一條自動回滾日誌。

也就說明,我們剛剛在 VolunteerIndexTabPage 觸發的 『VolunteerIndexTabPage 調用了 codePush.sync』起作用了。成功刪除了 Pending 標記位。避免了 codePush 的自動回滾 bug。

11. 再次殺死 App,重新進入 DailyQuestion 頁,你還是可以看到 Toast 『DailyQuestion 頁。codePush 資源包更新成功』。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章