写给原生开发的code-push 热更新流程【源码】【react-native】

前提

  • 通过react-native官方脚手架初始化项目
  • 项目内集成react-native-code-push8.1.0版本

本文的源码主要以js为主,iOS为辅,没有Android代码。原生代码,仅仅停留在原生模块暴露给js使用的方法。这种方法名iOS和Android都是同名的,Android去源码搜索同名方法即可,不影响对本文的理解

流程概括

  • 1、检查更新
  • 2、下载更新
  • 3、安装更新
  • 4、埋点上报逻辑

检查更新

起点:
在js端发起,js在页面入口调用 CodePush.sync()

// 自调用函数,返回值是一个函数。当外面调用时,即调用的当前函数返回的函数
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;
    // 核心逻辑都在这个函数里
    // 先生成一个promise
    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
    // 然后调用这个promise函数,这个函数里实现了核心逻辑
    syncPromise
      .then(setSyncCompleted)
      .catch(setSyncCompleted);

    return syncPromise;
  };
})();

概括一下这个函数的逻辑如下:

  • 添加是否在更新的过程中的标记
  • 进入更新过程中
  • 把整个更新的逻辑,包装成一个promise,更新结束后修改标记,表示不在更新过程中
  • 然后把这个promise返回去
    通过上面的梳理,可以看到核心逻辑都在syncPromise中,接下来我们看一下syncPromise做了什么。函数的实现在syncInternal
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) {
          // 1、检查更新
          case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
            log("Checking for update.");
            break;
          // 2、等待用户操作
          case CodePush.SyncStatus.AWAITING_USER_ACTION:
            log("Awaiting user action.");
            break;
          // 3、下载文件
          case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
            log("Downloading package.");
            break;
          // 4、安装更新
          case CodePush.SyncStatus.INSTALLING_UPDATE:
            log("Installing update.");
            break;
          // 5、更新完成
          case CodePush.SyncStatus.UP_TO_DATE:
            log("App is up to date.");
            break;
          // 6、用户取消更新
          case CodePush.SyncStatus.UPDATE_IGNORED:
            log("User cancelled the update.");
            break;
          // 7、更新安装完毕
          case CodePush.SyncStatus.UPDATE_INSTALLED:
            // 8、下次启动的时候,应用更新内容
            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) {
              // 9、应用进去后台的若干秒后,应用更新内容
              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 {
                // 10、下次resumes的时候,应用更新内容
                log("Update is installed and will be run when the app next resumes.");
              }
            }
            break;
          // 11、出现未知错误
          case CodePush.SyncStatus.UNKNOWN_ERROR:
            log("An unknown error occurred.");
            break;
        }
      };

  try {
    // 发个通知出去,这个通知的本质是一个上报,上报服务器本地的一些部署的信息。
    await CodePush.notifyApplicationReady();
    // 修改状态为检查更新
    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
    // 检查更新的逻辑,返回值是新版本的信息。这个信息里通过mixin的方式添加了download的函数,便于后续下载更新
    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

    // 声明一个函数,内部包含下载更新并安装的逻辑
    const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
      // 下载更新,下载成功后,本次更新就变成了本地包,所以返回值是本地包内容(本地更新的内容)内部会添加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;
    };

    // 判断当前的更新是否要被忽略
    const updateShouldBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);

    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) {
      // 如果需要展示dialog,让用户来操作如何更新
      // 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 dialogButtons = [];

        if (remotePackage.isMandatory) {
          message = syncOptions.updateDialog.mandatoryUpdateMessage;
          installButtonText = syncOptions.updateDialog.mandatoryContinueButtonLabel;
        } else {
          message = syncOptions.updateDialog.optionalUpdateMessage;
          installButtonText = syncOptions.updateDialog.optionalInstallButtonLabel;
          // Since this is an optional update, add a button
          // to allow the end-user to ignore it
          dialogButtons.push({
            text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
            onPress: () => {
              syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_IGNORED);
              resolve(CodePush.SyncStatus.UPDATE_IGNORED);
            }
          });
        }
        
        // Since the install button should be placed to the 
        // right of any other button, add it last
        dialogButtons.push({
          text: installButtonText,
          onPress:() => {
            doDownloadAndInstall()
              .then(resolve, reject);
          }
        })

        // If the update has a description, and the developer
        // explicitly chose to display it, then set that as the message
        if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
          message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
        }

        syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
        Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
      });
    } else {
      // 开始下载更新并安装
      return await doDownloadAndInstall();
    }
  } catch (error) {
    syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
    log(error.message);
    throw error;
  }
};

概括一下上述逻辑:

  • 声明更新相关的配置信息syncOptions
  • 声明不同状态的回调函数:通过这个回调函数,可以发现,整个更新过程包括多个阶段(检查更新、下载更新、安装更新、应用更新)其中在应用更新有多个时机可供选择(重启app生效、应用进去后台n秒后生效、resumes时生效)
  • 发个上报,上报部署的信息,如果之前已经下载了最新版本,没有应用,在第一次应用的时候,会上报一次,后续不会上报
  • 开始检查更新const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
  • 如果没有更新或这个更新被忽略,直接获取当前本地包的信息const currentPackage = await CodePush.getCurrentPackage();,然后检查本地包是正在安装更新中,还是已更新到最新
  • 如果需要弹出dialog,就需要让用户来操作如何更新
  • 如果有新版本且不需要弹出dialog,则自动开始下载更新并安装await doDownloadAndInstall(),下载成功的时候,还会有一个下载成功的上报。

接下来我们需要分别分一下各个关键环节的逻辑

  • 检查更新
  • 获取本地包信息
  • 下载并安装更新

一、检查更新


async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
  /*
   * 在我们询问服务器是否存在更新之前,
   * 我们需要从本机端检索三项信息:部署密钥、应用程序版本(例如 1.0.1)和当前正在运行的更新的哈希值(如果有)。
   * 这允许客户端仅接收针对其特定部署和版本且实际上与其已安装的 CodePush 更新不同的更新。
   */
  // 从本地获取配置信息
  const nativeConfig = await getConfiguration();
  /*
   * 如果显式提供了部署密钥,
   * 那么让我们覆盖从应用程序本机端检索到的部署密钥。 
   * 这允许在不同的部署中动态“重定向”最终用户(例如内部人员的早期访问部署)。
   */
  const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
  const sdk = getPromisifiedSdk(requestFetchAdapter, config);

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

  /*
   * 如果应用程序之前安装了更新,
   * 并且该更新针对当前正在运行的同一应用程序版本,
   * 那么我们希望使用其包哈希来确定服务器上是否已发布新版本。 
   * 否则,我们只需将应用程序版本发送到服务器,
   * 因为我们对当前二进制版本的任何更新感兴趣,而不管哈希值如何。
   */
  let queryPackage;
  if (localPackage) {
    queryPackage = localPackage;
  } else {
    queryPackage = { appVersion: config.appVersion };
    if (Platform.OS === "ios" && config.packageHash) {
      queryPackage.packageHash = config.packageHash;
    }
  }

  // 检查更新的核心逻辑,如果有更新,此时update就有值
  const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);

  /*
   * checkForUpdate 有四种情况会解析为 null:
    * ------------------------------------------------- ----------------
    * 1) 服务器说没有更新。 这是最常见的情况。
    * 2) 服务器表示有更新,但需要更新的二进制版本(native app版本)。
    *     当最终用户运行比可用版本更旧的二进制版本时,就会发生这种情况,
    *     并且 CodePush 正在确保他们不会获得可能无法获得的更新 与他们正在运行的内容不兼容。
    * 3) 服务器说有更新,但更新的哈希值与当前运行的更新相同。 
    *     这应该永远不会发生,除非服务器中存在错误,
    *     但我们添加此检查只是为了仔细检查客户端应用程序是否能够应对更新检查的潜在问题。
    * 4) 服务器说有更新,但更新的哈希值与二进制文件当前运行版本的哈希值相同。 
    *     这应该只发生在 Android 中 - 
    *     与 iOS 不同,我们不会将二进制文件的哈希附加到 updateCheck 请求,
    *     因为我们希望避免必须针对二进制版本安装差异更新,而这在 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 {
    // 生成remotePackage,会把download方法添加进去,也会添加下载成功的回调
    const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
    // 然后加一个安装失败的回调,这个回调会直接调用到Native的逻辑
    remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
    // 把key修改为显示指定的key或者是从native获取到的key
    remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
    return remotePackage;
  }
}

上述的逻辑概括如下:

  • 从native获取配置信息const nativeConfig = await getConfiguration();
  • 获取本地的currentPackage信息 const localPackage = await module.exports.getCurrentPackage();
  • 通过本地信息生成网络请求的参数
  • 检查远程服务器上是否有更新const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);.没有更新的四种情况:1、远程没有更新。2、远程有更新,但是本地app版本过低。3、远程有更新,但是和当前本地最新版本的内容一致。4、远程有更新,但是和本地内置的内容hash一致。这种只会发生在Android。
  • 如果有更新的话,以远程更新的信息为基础,再添加几个字段,然后返回出去。添加的内容主要是下载方法和下载成功的回调,以及是否正在下载的标记

1、从native获取配置

const getConfiguration = (() => {
  let config;
  return async function getConfiguration() {
    if (config) {
      return config;
    } else if (testConfig) {
      return testConfig;
    } else {
      config = await NativeCodePush.getConfiguration();
      return config;
    }
  }
})();

优先使用缓存,如果没有缓存数据,使用测试数据,没有测试数据,则从native获取config = await NativeCodePush.getConfiguration();

iOS:

RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve
                          rejecter:(RCTPromiseRejectBlock)reject)
{
    NSDictionary *configuration = [[CodePushConfig current] configuration];
    NSError *error;
    if (isRunningBinaryVersion) {
        // isRunningBinaryVersion will not get set to "YES" if running against the packager.
        NSString *binaryHash = [CodePushUpdateUtils getHashForBinaryContents:[CodePush binaryBundleURL] error:&error];
        if (error) {
            CPLog(@"Error obtaining hash for binary contents: %@", error);
            resolve(configuration);
            return;
        }

        if (binaryHash == nil) {
            // The hash was not generated either due to a previous unknown error or the fact that
            // the React Native assets were not bundled in the binary (e.g. during dev/simulator)
            // builds.
            resolve(configuration);
            return;
        }

        NSMutableDictionary *mutableConfiguration = [configuration mutableCopy];
        [mutableConfiguration setObject:binaryHash forKey:PackageHashKey];
        resolve(mutableConfiguration);
        return;
    }

    resolve(configuration);
}

Android


2、获取本地的currentPackage信息

async function getCurrentPackage() {
  return await getUpdateMetadata(CodePush.UpdateState.LATEST);
}

async function getUpdateMetadata(updateState) {
  let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
  if (updateMetadata) {
    updateMetadata = {...PackageMixins.local, ...updateMetadata};
    updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
    updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
  }
  return updateMetadata;
}

上述逻辑概括:

  • 从native获取updateMetadata
  • 然后给这个updateMetadata添加一些回调函数:安装失败的回调、第一次运行的回调
    对应到原生的代码如下:

/*
 * This method is the native side of the CodePush.getUpdateMetadata method.
 */
RCT_EXPORT_METHOD(getUpdateMetadata:(CodePushUpdateState)updateState
                           resolver:(RCTPromiseResolveBlock)resolve
                           rejecter:(RCTPromiseRejectBlock)reject)
{
    NSError *error;
    NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy];

    if (error) {
        return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
    } else if (package == nil) {
        // The app hasn't downloaded any CodePush updates yet,
        // so we simply return nil regardless if the user
        // wanted to retrieve the pending or running update.
        return resolve(nil);
    }

    // We have a CodePush update, so let's see if it's currently in a pending state.
    BOOL currentUpdateIsPending = [[self class] isPendingUpdate:[package objectForKey:PackageHashKey]];

    if (updateState == CodePushUpdateStatePending && !currentUpdateIsPending) {
        // The caller wanted a pending update
        // but there isn't currently one.
        resolve(nil);
    } else if (updateState == CodePushUpdateStateRunning && currentUpdateIsPending) {
        // The caller wants the running update, but the current
        // one is pending, so we need to grab the previous.
        resolve([CodePushPackage getPreviousPackage:&error]);
    } else {
        // The current package satisfies the request:
        // 1) Caller wanted a pending, and there is a pending update
        // 2) Caller wanted the running update, and there isn't a pending
        // 3) Caller wants the latest update, regardless if it's pending or not
        if (isRunningBinaryVersion) {
            // This only matters in Debug builds. Since we do not clear "outdated" updates,
            // we need to indicate to the JS side that somehow we have a current update on
            // disk that is not actually running.
            [package setObject:@(YES) forKey:@"_isDebugOnly"];
        }

        // Enable differentiating pending vs. non-pending updates
        [package setObject:@(currentUpdateIsPending) forKey:PackageIsPendingKey];
        resolve(package);
    }
}

iOS

/*
 * This method isn't publicly exposed via the "react-native-code-push"
 * module, and is only used internally to populate the RemotePackage.failedInstall property.
 */
RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash
                         resolve:(RCTPromiseResolveBlock)resolve
                          reject:(RCTPromiseRejectBlock)reject)
{
    BOOL isFailedHash = [[self class] isFailedHash:packageHash];
    resolve(@(isFailedHash));
}
/*
 * This method isn't publicly exposed via the "react-native-code-push"
 * module, and is only used internally to populate the LocalPackage.isFirstRun property.
 */
RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash
                     resolve:(RCTPromiseResolveBlock)resolve
                    rejecter:(RCTPromiseRejectBlock)reject)
{
    NSError *error;
    BOOL isFirstRun = _isFirstRunAfterUpdate
                        && nil != packageHash
                        && [packageHash length] > 0
                        && [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]];

    resolve(@(isFirstRun));
}

Android



3、调用接口从服务端获取更新信息

queryUpdateWithCurrentPackage的实现如下:(代码在code-push这个包里,不在react-native-code-push这个包里)

AcquisitionManager.prototype.queryUpdateWithCurrentPackage = function (currentPackage, callback) {
        var _this = this;
        if (!currentPackage || !currentPackage.appVersion) {
            throw new code_push_error_1.CodePushPackageError("Calling common acquisition SDK with incorrect package"); // Unexpected; indicates error in our implementation
        }
        // 拼装请求的参数
        var updateRequest = {
            deployment_key: this._deploymentKey,
            app_version: currentPackage.appVersion,
            package_hash: currentPackage.packageHash,
            is_companion: this._ignoreAppVersion,
            label: currentPackage.label,
            client_unique_id: this._clientUniqueId
        };
        // 生成请求的URL
        var requestUrl = this._serverUrl + this._publicPrefixUrl + "update_check?" + queryStringify(updateRequest);
        // 发送网络请求
        this._httpRequester.request(0 /* Http.Verb.GET */, requestUrl, function (error, response) {
            if (error) {
                callback(error, /*remotePackage=*/ null);
                return;
            }
            if (response.statusCode !== 200) {
                var errorMessage = void 0;
                if (response.statusCode === 0) {
                    errorMessage = "Couldn't send request to ".concat(requestUrl, ", xhr.statusCode = 0 was returned. One of the possible reasons for that might be connection problems. Please, check your internet connection.");
                }
                else {
                    errorMessage = "".concat(response.statusCode, ": ").concat(response.body);
                }
                callback(new code_push_error_1.CodePushHttpError(errorMessage), /*remotePackage=*/ null);
                return;
            }
            try {
                var responseObject = JSON.parse(response.body);
                // 解析数据
                var updateInfo = responseObject.update_info;
            }
            catch (error) {
                callback(error, /*remotePackage=*/ null);
                return;
            }
            if (!updateInfo) {
                callback(error, /*remotePackage=*/ null);
                return;
            }
            else if (updateInfo.update_app_version) {
                callback(/*error=*/ null, { updateAppVersion: true, appVersion: updateInfo.target_binary_range });
                return;
            }
            else if (!updateInfo.is_available) {
                callback(/*error=*/ null, /*remotePackage=*/ null);
                return;
            }
            // 生成remotePackage
            var remotePackage = {
                deploymentKey: _this._deploymentKey,
                description: updateInfo.description,
                label: updateInfo.label,
                appVersion: updateInfo.target_binary_range,
                isMandatory: updateInfo.is_mandatory,
                packageHash: updateInfo.package_hash,
                packageSize: updateInfo.package_size,
                downloadUrl: updateInfo.download_url
            };
            callback(/*error=*/ null, remotePackage);
        });
    };

概括如下:

  • 组装参数
  • 发送请求
  • 解析数据
  • 组装出remotePackage,返回回去

二、下载并安装更新

此处逻辑对应的doDownloadAndInstall的实现

const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
      // 下载逻辑
      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;
    };

概括如下:

  • 下载更新
  • 安装更新

1、 下载更新

对应
const localPackage = await remotePackage.download(downloadProgressCallback);
的逻辑。
实际的download实现如下:

 async download(downloadProgressCallback) {
        if (!this.downloadUrl) {
          throw new Error("Cannot download an update without a download url");
        }

        let downloadProgressSubscription;
        // 添加下载进度的监听
        if (downloadProgressCallback) {
          const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
          // 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}`);
            });
          }
          // 次数的返回值,除了会把downloadedPackage信息放进来以为,还会添加install函数,以及是否正在install的标记。下一步可直接调用install
          return { ...downloadedPackage, ...local };
        } finally {
          downloadProgressSubscription && downloadProgressSubscription.remove();
        }
      },

概括如下:

  • 添加对下载进度监听
  • 把参数传给原生端,原生实现下载功能
  • 当下载完毕后,上报一下下载完成
  • 最后把原生的返回值,添加install方法后,返回出去

iOS

/*
 * This is native-side of the RemotePackage.download method
 */
RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
                  notifyProgress:(BOOL)notifyProgress
                        resolver:(RCTPromiseResolveBlock)resolve
                        rejecter:(RCTPromiseRejectBlock)reject)
{
    NSDictionary *mutableUpdatePackage = [updatePackage mutableCopy];
    NSURL *binaryBundleURL = [CodePush binaryBundleURL];
    if (binaryBundleURL != nil) {
        [mutableUpdatePackage setValue:[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL]
                                forKey:BinaryBundleDateKey];
    }

    if (notifyProgress) {
        // Set up and unpause the frame observer so that it can emit
        // progress events every frame if the progress is updated.
        _didUpdateProgress = NO;
        self.paused = NO;
    }

    NSString * publicKey = [[CodePushConfig current] publicKey];

    [CodePushPackage
        downloadPackage:mutableUpdatePackage
        expectedBundleFileName:[bundleResourceName stringByAppendingPathExtension:bundleResourceExtension]
        publicKey:publicKey
        operationQueue:_methodQueue
        // The download is progressing forward
        progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
            // Update the download progress so that the frame observer can notify the JS side
            _latestExpectedContentLength = expectedContentLength;
            _latestReceivedConentLength = receivedContentLength;
            _didUpdateProgress = YES;

            // If the download is completed, stop observing frame
            // updates and synchronously send the last event.
            if (expectedContentLength == receivedContentLength) {
                _didUpdateProgress = NO;
                self.paused = YES;
                [self dispatchDownloadProgressEvent];
            }
        }
        // The download completed
        doneCallback:^{
            NSError *err;
            NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];

            if (err) {
                return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
            }
            resolve(newPackage);
        }
        // The download failed
        failCallback:^(NSError *err) {
            if ([CodePushErrorUtils isCodePushError:err]) {
                [self saveFailedUpdate:mutableUpdatePackage];
            }

            // Stop observing frame updates if the download fails.
            _didUpdateProgress = NO;
            self.paused = YES;
            reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
        }];
}

Android


2、安装更新

对应
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => { syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED); });
实际的install实现如下:

 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
      // 调用原生方法,实现install的逻辑
      await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
      updateInstalledCallback && updateInstalledCallback();
      // 根据生效时机的配置,操作如何生效
      if (installMode == NativeCodePush.codePushInstallModeImmediate) {
        // 如果是立即生效,则调用原生方法,立刻生效
        NativeCodePush.restartApp(false);
      } else {
        // 如果不是立即生效,也是调用原生方法,做一些数据的清除操作
        NativeCodePush.clearPendingRestart();
        localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
      }
    },

概括:

  • 把相关参数传给原生端,原生端实现install功能
  • 安装完毕后,根据生效时机,调用不同的后续处理逻辑
  • 如果需要立即生效,则调用原生模块提供的restartApp方法
  • 如果不需要立刻生效,则调用原生模块提供的clearPendingRestart方法,清除没用的数据

iOS


/*
 * This method is the native side of the LocalPackage.install method.
 */
RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
                    installMode:(CodePushInstallMode)installMode
      minimumBackgroundDuration:(int)minimumBackgroundDuration
                       resolver:(RCTPromiseResolveBlock)resolve
                       rejecter:(RCTPromiseRejectBlock)reject)
{
    NSError *error;
    [CodePushPackage installPackage:updatePackage
                removePendingUpdate:[[self class] isPendingUpdate:nil]
                              error:&error];

    if (error) {
        reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
    } else {
        [self savePendingUpdate:updatePackage[PackageHashKey]
                      isLoading:NO];

        _installMode = installMode;
        if (_installMode == CodePushInstallModeOnNextResume || _installMode == CodePushInstallModeOnNextSuspend) {
            _minimumBackgroundDuration = minimumBackgroundDuration;

            if (!_hasResumeListener) {
                // Ensure we do not add the listener twice.
                // Register for app resume notifications so that we
                // can check for pending updates which support "restart on resume"
                [[NSNotificationCenter defaultCenter] addObserver:self
                                                         selector:@selector(applicationDidBecomeActive)
                                                             name:UIApplicationDidBecomeActiveNotification
                                                           object:RCTSharedApplication()];
                                                           
                [[NSNotificationCenter defaultCenter] addObserver:self
                                                         selector:@selector(applicationWillEnterForeground)
                                                             name:UIApplicationWillEnterForegroundNotification
                                                           object:RCTSharedApplication()];

                [[NSNotificationCenter defaultCenter] addObserver:self
                                                         selector:@selector(applicationWillResignActive)
                                                             name:UIApplicationWillResignActiveNotification
                                                           object:RCTSharedApplication()];

                _hasResumeListener = YES;
            }
        }

        // Signal to JS that the update has been applied.
        resolve(nil);
    }
}

Android


数据上报

除了文件的下载安装等路逻辑以外,还需要注意一些关键信息的上报,当前主要是两个节点

  • 下载成功: 表示本地已经将服务器上的更新,下载到本地。
  • 部署成功: 表示本地下载的更新内容已经生效。

下载成功

 AcquisitionManager.prototype.reportStatusDownload = function (downloadedPackage, callback) {
        var url = this._serverUrl + this._publicPrefixUrl + "report_status/download";
        var body = {
            client_unique_id: this._clientUniqueId,
            deployment_key: this._deploymentKey,
            label: downloadedPackage.label
        };
        this._httpRequester.request(2 /* Http.Verb.POST */, url, JSON.stringify(body), function (error, response) {
            if (callback) {
                if (error) {
                    callback(error, /*not used*/ null);
                    return;
                }
                if (response.statusCode !== 200) {
                    callback(new code_push_error_1.CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null);
                    return;
                }
                callback(/*error*/ null, /*not used*/ null);
            }
        });
    };

部署成功

 AcquisitionManager.prototype.reportStatusDeploy = function (deployedPackage, status, previousLabelOrAppVersion, previousDeploymentKey, callback) {
        var url = this._serverUrl + this._publicPrefixUrl + "report_status/deploy";
        var body = {
            app_version: this._appVersion,
            deployment_key: this._deploymentKey
        };
        if (this._clientUniqueId) {
            body.client_unique_id = this._clientUniqueId;
        }
        if (deployedPackage) {
            body.label = deployedPackage.label;
            body.app_version = deployedPackage.appVersion;
            switch (status) {
                case AcquisitionStatus.DeploymentSucceeded:
                case AcquisitionStatus.DeploymentFailed:
                    body.status = status;
                    break;
                default:
                    if (callback) {
                        if (!status) {
                            callback(new code_push_error_1.CodePushDeployStatusError("Missing status argument."), /*not used*/ null);
                        }
                        else {
                            callback(new code_push_error_1.CodePushDeployStatusError("Unrecognized status \"" + status + "\"."), /*not used*/ null);
                        }
                    }
                    return;
            }
        }
        if (previousLabelOrAppVersion) {
            body.previous_label_or_app_version = previousLabelOrAppVersion;
        }
        if (previousDeploymentKey) {
            body.previous_deployment_key = previousDeploymentKey;
        }
        callback = typeof arguments[arguments.length - 1] === "function" && arguments[arguments.length - 1];
        this._httpRequester.request(2 /* Http.Verb.POST */, url, JSON.stringify(body), function (error, response) {
            if (callback) {
                if (error) {
                    callback(error, /*not used*/ null);
                    return;
                }
                if (response.statusCode !== 200) {
                    callback(new code_push_error_1.CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null);
                    return;
                }
                callback(/*error*/ null, /*not used*/ null);
            }
        });
    };

上报的本质就是调用api,发送一次请求,把一些参数传给服务器,服务器会做好记录,便于统计。

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

推荐阅读更多精彩内容