美文网首页
CodePush 流程介绍及源码分析

CodePush 流程介绍及源码分析

作者: FingerStyle | 来源:发表于2022-01-09 14:41 被阅读0次

一、业务流程

1.正常更新流程

1)默认情况下APP如果是第一次启动,则更新完成后立刻重启bridge,否则等到下次启动才重启bridge(可以设置强制更新,但重启会白屏)
2)如果下载过程中因断网、APP退出而导致下载失败的,已下载的数据作废,需要重新下载
3)下载过程中不能连代理,否则会下载失败(https证书校验通不过)
4)可以分包下载,不过需要修改源码,更新逻辑比较复杂

image.png

1.1 检查更新:这是codepush的第一步,也是最重要的一步!!
主要代码在JS这边,从RN包加载后,就开始调用 CodePushUtil 的init方法开始检查更新,然后调用RN自带的网路组件去请求codepush服务器。 客户端把本地RN包的版本号(label)、hash值和APP版本传给服务端, 检查是否有新的包可以下载。 如果有,则会返回对应的地址和其他相应字段,如果没有,则地址为空字符串

返回数据(有新版本):

{
    "updateInfo": {
        "downloadURL": "https://cstatic.xxx.net/code-push-download/lh9RADUHUusZEaLk9T1T5j6OnLV_",
        "downloadUrl": "https://cstatic.xxx.net/code-push-download/lh9RADUHUusZEaLk9T1T5j6OnLV_",
        "description": "",
        "isAvailable": true,
        "isDisabled": false,
        "isMandatory": false,
        "appVersion": "4.2.3",
        "targetBinaryRange": ">=3.5.9",
        "packageHash": "bfa5e47bf38cceeaee107d390dd9e004fc38ce54558b53ee4596b2fad0e838fe",
        "label": "v1315",
        "packageSize": 5806711,
        "updateAppVersion": false,
        "shouldRunBinaryVersion": false
    }
}

返回数据(无新版本):

{
    "updateInfo": {
        "downloadUrl": "",
        "description": "",
        "isAvailable": false,
        "isDisabled": true,
        "isMandatory": false,
        "appVersion": "4.2.3",
        "targetBinaryRange": "",
        "packageHash": "",
        "label": "",
        "packageSize": 0,
        "updateAppVersion": false,
        "shouldRunBinaryVersion": false
    }
}

返回的结果会保存在本地对应版本的 的 app.json文件中,路径为 沙盒的文档目录(不会被系统自动清理) /CodePush/ 本地包的hash值/app.json。

请求逻辑代码主要在CodePush.js的sync->syncInternal-> checkForUpdate方法内,实际请求的代码是放在 acquisition-sdk 的queryUpdateWithCurrentPackage 方法内。

这里简单看一下syncInternal方法的实现,这个方法比较长,拆成三段来看:

首先是初始化syncOptions和申明syncStatusChangeCallback,这个syncStatusChangeCallback是通过参数外部传入的,后面会多次调用

/*
 * The syncInternal method provides a simple, one-line experience for
 * incorporating the check, download and installation of an update.
 *
 * It simply composes the existing API methods together and adds additional
 * support for respecting mandatory updates, ignoring previously failed
 * releases, and displaying a standard confirmation UI to the end-user
 * when an update is available.
 */
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, downloadFailedCallback, handleBinaryVersionMismatchCallback, syncCallbackOptions = {}) {
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
  };


  const { checkForUpdateOptions = {}, downloadOptions = {}, oneSelfAchieveOptions = {} } = syncCallbackOptions

  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 开始一直到doDownloadAndInstall这个方法的定义结束,这里先是把syncStatus改为CHECKING_FOR_UPDATE,调用checkForUpdate。

然后定义了doDownloadAndInstall,这里面是下载和安装的实际操作,包括调用了 remotePackage.download 下载RN包,调用localPackage.install进行安装,

......
try {
    await CodePush.notifyApplicationReady();


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

    const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);

      let localPackage
      downloadOptions.downloadStartHandler && downloadOptions.downloadStartHandler()
      let isSuccess = true
      try {
        localPackage = await remotePackage.download(downloadProgressCallback, downloadFailedCallback);
        // 连了代理会下载不到
        // if (!localPackage) {
        //   isSuccess = false
        // }
      } catch (e) {
        isSuccess = false
      }
      downloadOptions.downloadEndHandler && downloadOptions.downloadEndHandler(isSuccess);


      // 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, syncOptions.isOneSelfAchieve, () => {
        syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
      }, () => {
        //后加  下载完成 还未重新加载
        oneSelfAchieveOptions.oneSelfAchieveCallback && oneSelfAchieveOptions.oneSelfAchieveCallback()
      });

      return CodePush.SyncStatus.UPDATE_INSTALLED;
    };
......

第三段是从 updateShouldBeIgnored 一直到方法结束,这里根据 checkForUpdate获取到的remotePackage和 shouldUpdateBeIgnored的结果分为三种情况:

一是没有远程包或者之前这个包回滚过,这时候不会更新,而是把状态改为UPDATE_INSTALLED或者UP_TO_DATE;

二是需要用户确认更新,这时候会有弹窗出来,当用户点击之后再更新(我们没有用到,可以忽略)

三是默认情况,调用doDownloadAndInstall更新

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

1.2 下载bundle包
根据步骤(1)返回的downloadUrl进行下载,这个过程主要是在原生这边做的,JS只是触发原生的downloadUpdate方法,并监听下载完成的回调,在回调里面触发RN包的安装和状态上报。
代码在package-mixins.js 的 remote.download方法内

1.3 安装bundle包
把步骤(2)得到的bundle包进行解压并拷贝到指定目录。这个过程也是以原生为主,JS这边触发了原生的installUpdate方法,并在这个方法执行完成后调用restartApp重启 bridge。
代码在package-mixins.js 的 local.install方法内

1.4 上报下载状态
上报下载成功的状态,主要是用于统计,失败或者无更新时不会上报。这个接口会修改到服务端的数据库,是引发服务器性能瓶颈的一个点。
接口地址: https://codepush.xx.net/reportStatus/download

请求参数

{
"clientUniqueId": "A25E48C6-1766-431B-B979-6F1CD2607579",
"deploymentKey": "57w3cawU4Q93X95zdP84FVY3YIxQ4ksvOXqog",
"label": "v1323",
}

具体代码在 acquisition-sdk 的 reportStatusDownload 方法内

1.5 上报安装状态
上报安装成功还是失败的状态,主要是用于统计,如果是安装失败,还把这个包标记为不可更新,下次RN包出问题时会根据这个标记来决定是否回滚。这个接口会修改到服务端的数据库,是引发服务器性能瓶颈的一个点。
接口地址: https://codepush.xx.net/reportStatus/deploy

请求参数

{
"appVersion": "4.2.3",
"deploymentKey": "57w3cawU4Q93X95zdP84FVY3YIxQ4ksvOXqog",
"clientUniqueId": "A25E48C6-1766-431B-B979-6F1CD2607579",
"label": "v1323",
"status": "DeploymentSucceeded",
"previousLabelOrAppVersion": "v1315",
"previousDeploymentKey": "57w3cawU4Q93X95zdP84FVY3YIxQ4ksvOXqog",
}

主要代码在Codepush.js 的 getNewStatusReport 方法(从原生获取上报状态)和 acquisition-sdk 的 reportStatusDeploy 方法内(发请求), 设置回滚标记的写在原生的 setLatestRollbackInfo 方法内
下面简单分析下getNewStatusReport在iOS端的实现:

/*
* This method is checks if a new status update exists (new version was installed,
* or an update failed) and return its details (version label, status).
*/
RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
   if (needToReportRollback) { //如果当前包需要上报回滚(更新后发生严重错误导致无法运.行),则通过NSUserDefaults获取到最近的更新失败的包信息,返回给JS
      needToReportRollback = NO;
      NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
      NSMutableArray *failedUpdates = [preferences objectForKey:FailedUpdatesKey];
      if (failedUpdates) {
         NSDictionary *lastFailedPackage = [failedUpdates lastObject];
         if (lastFailedPackage) {
            resolve([CodePushTelemetryManager getRollbackReport:lastFailedPackage]);
            return;
         }
      }
   } else if (_isFirstRunAfterUpdate) {  //如果是更新后第一次运行(即更新成功),则返回这个包的信息给JS
      NSError *error;
      NSDictionary *currentPackage = [CodePushPackage getCurrentPackage:&error];
      if (!error && currentPackage) {
      resolve([CodePushTelemetryManager getUpdateReport:currentPackage]);
      return;
    }
  } else if (isRunningBinaryVersion) { //如果运行的是本地包,则把本地包信息给JS (APP版本之类的信息)
    NSString *appVersion = [[CodePushConfig current] appVersion];
    resolve([CodePushTelemetryManager getBinaryUpdateReport:appVersion]);
    return;
  } else { //如果有重新上报的标记,则把重新上报的状态给JS(因为超时或其他原因导致需重新上报)
    NSDictionary *retryStatusReport = [CodePushTelemetryManager getRetryStatusReport];
    if (retryStatusReport) {
      resolve(retryStatusReport);
      return;
    }
  }
//默认什么都不返回
  resolve(nil);
}

这里回滚的情况比较复杂,下面异常更新流程会详细展开

2. 异常更新流程

1)本地有两到三个包,一个跟随APP发布的默认包,另外一到两个通过codepush下载的最新包。如果最新包出错,则回滚到上一个包,如果还是出错,则加载默认包。
2)本地包加载出错时,默认会崩溃,可以加拦截让其不崩溃,但会白屏。
3)一个用户出现回滚后会把出错的信息上报服务端,服务端标记这个RN包版本有问题,后续这个用户再也不会下载这个包,除非手动修改服务端标记。

image.png

2.1 本地有两到三个包,一个跟随APP发布的默认包,另外一到两个是通过codepush下载的RN包,其中一个是最近更新的包,另一个是上一次更新的包(如果第一次更新,则没有这个包)。

如果最新包出错,则回滚到上一个包,如果上一个包不存在或者还是出错, 到回滚到默认包。

2.2 本地包出错时,默认会崩溃,我们加了拦截,让其不崩溃,但会白屏,并自动重启bridge。

2.3 一个用户出现回滚后会把出错的信息上报服务端,服务端标记这个RN包版本有问题,后续这个用户再也不会下载这个包,除非手动修改服务端标记。

针对回滚的流程, 我们来看下具体的代码,首先是needToReportRollback这个bool变量,前面提到在getNewStatusReport 这个方法内会用到这个标记,那这个标记什么赋值的呢?

先看下initializeUpdateAfterRestart 这个方法,这是是在CodePush启动之后就立即调用,在updateIsLoading为true的时候将这个标记改为YES(也就是true)。

这个updateIsLoading变量在codepush的一个生命周期内只会修改一次,默认是true,当安装成功后会改为false(见installUpdate方法)。

在标记需要上报回滚之后,就调用rollbackPackage进行本地回滚了

/*
 * This method is used when the app is started to either
 * initialize a pending update or rollback a faulty update
 * to the previous version.
 */
- (void)initializeUpdateAfterRestart
{
#ifdef DEBUG
    [self clearDebugUpdates];
#endif
    self.paused = YES;
    NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
    NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
    if (pendingUpdate) {
        _isFirstRunAfterUpdate = YES;//标记是更新后第一次运行
        BOOL updateIsLoading = [pendingUpdate[PendingUpdateIsLoadingKey] boolValue];
        if (updateIsLoading) {
            NSDictionary *installUpdate = [preferences objectForKey:PendingInstallKey];
            if (installUpdate) {
                BOOL updateIsLoading = [installUpdate[PendingUpdateIsInstallKey] boolValue];
                if (!updateIsLoading) {
                    // Pending update was initialized, but notifyApplicationReady was not called.
                    // Therefore, deduce that it is a broken update and rollback.
                    CPLog(@"Update did not finish loading the last time, rolling back to a previous version.");
                    needToReportRollback = YES;//标记需要上报回滚
                    [self rollbackPackage];//进行本地回滚
                }
            }else{
                [CodePush removePendingUpdate];
                [CodePush removePendingInstall];
            }
        } else {
            // 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.
           //默认updateIsLoading设置为true
            [self savePendingUpdate:pendingUpdate[PendingUpdateHashKey]
                          isLoading:YES];
        }
    }
}

/*
 * 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)
{
    [[NSNotificationCenter defaultCenter] postNotificationName:CodePushWillInstallUpdateNotification object:nil userInfo:updatePackage];
    NSError *error;
    //这一步进行安装,如果出错会通过error参数把错误返回出来
    [CodePushPackage installPackage:updatePackage
                removePendingUpdate:[[self class] isPendingUpdate:nil]
                              error:&error];
    [[NSNotificationCenter defaultCenter] postNotificationName:CodePushDidInstallUpdateNotification object:nil userInfo:updatePackage];
   //如果安装出错,则返回异常,否则修改updateIsLoading的状态和installMode,并监听APP重新返回前台的通知(收到通知后重启bundle)
    if (error) {
        [self saveInstallFailUpdate:updatePackage[PackageHashKey] isInstall:NO];
        reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
    } else {
        //修改updateIsLoading为false. 
        [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);
    }
}

回滚的具体实现 , 注意一下这里 failedPackage 的信息被写到了本地,在上报安装状态时会通过 getNewStatusReport拿到,同步给服务端,以确保有问题的包不会再次下发。

CodePush.m

/*
 * This method is used when an update has failed installation
 * and the app needs to be rolled back to the previous bundle.
 * This method is automatically called when the rollback timer
 * expires without the app indicating whether the update succeeded,
 * and therefore, it shouldn't be called directly.
 */
- (void)rollbackPackage
{
    NSError *error;
    NSDictionary *failedPackage = [CodePushPackage getCurrentPackage:&error];
    if (!failedPackage) {
        if (error) {
            CPLog(@"Error getting current update metadata during rollback: %@", error);
        } else {
            CPLog(@"Attempted to perform a rollback when there is no current update");
        }
    } else {
        // Write the current package's metadata to the "failed list"
        //本地记录更新失败的包,以便后续通过状态上报接口同步给服务端
        [self saveFailedUpdate:failedPackage];
    }

    // Rollback to the previous version and de-register the new update
    [CodePushPackage rollbackPackage]; //清除本地bundle 及所在文件夹(包括app.json),并把上一个包设置为当前包
    [CodePush removePendingUpdate];
    [CodePush removePendingInstall];
    [self loadBundle]; //重启bundle
}

/*
 * This method updates the React Native bridge's bundle URL
 * to point at the latest CodePush update, and then restarts
 * the bridge. This isn't meant to be called directly.
 */
- (void)loadBundle
{
    // This needs to be async dispatched because the bridge is not set on init
    // when the app first starts, therefore rollbacks will not take effect.
    dispatch_async(dispatch_get_main_queue(), ^{
        // If the current bundle URL is using http(s), then assume the dev
        // is debugging and therefore, shouldn't be redirected to a local
        // file (since Chrome wouldn't support it). Otherwise, update
        // the current bundle URL to point at the latest update
        if ([CodePush isUsingTestConfiguration] || ![super.bridge.bundleURL.scheme hasPrefix:@"http"]) {
            [super.bridge setValue:[CodePush bundleURL] forKey:@"bundleURL"];
        }
        //重启bridge,重新运行bundle
        [super.bridge reload];
    });
}

CodePushPackage.m

+ (void)rollbackPackage
{
    NSError *error;
    NSMutableDictionary *info = [self getCurrentPackageInfo:&error];
    if (!info) {
        CPLog(@"Error getting current package info: %@", error);
        return;
    }
    
    NSString *currentPackageFolderPath = [self getCurrentPackageFolderPath:&error];
    if (!currentPackageFolderPath) {
        CPLog(@"Error getting current package folder path: %@", error);
        return;
    }
    //清除本地bundle 及所在文件夹
    NSError *deleteError;
    BOOL result = [[NSFileManager defaultManager] removeItemAtPath:currentPackageFolderPath
                                               error:&deleteError];

    if (!result) {
        CPLog(@"Error deleting current package contents at %@ error %@", currentPackageFolderPath, deleteError);
    }
    //把上一个包设置为当前包,清除上一个包的信息
    [info setValue:info[@"previousPackage"] forKey:@"currentPackage"];
    [info removeObjectForKey:@"previousPackage"];
    
    [self updateCurrentPackageInfo:info error:&error];
    [[CodePushReport shared] reportRollback];
}

相关文章

网友评论

      本文标题:CodePush 流程介绍及源码分析

      本文链接:https://www.haomeiwen.com/subject/zzyjoltx.html