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 资源包更新成功』。

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