CodePush热更新测试记录和使用笔记

643 阅读7分钟

我正在参加「掘金·启航计划」

以下是基于版本 7.1.0 进行记录总结的。

1. Code Push 各个更新状态说明

enum SyncStatus {
  UP_TO_DATE: 0, // The running app is up-to-date
  UPDATE_INSTALLED: 1, // The app had an optional/mandatory update that was successfully downloaded and is about to be installed.
  UPDATE_IGNORED: 2, // The app had an optional update and the end-user chose to ignore it
  UNKNOWN_ERROR: 3,
  SYNC_IN_PROGRESS: 4, // There is an ongoing "sync" operation in progress.
  CHECKING_FOR_UPDATE: 5,
  AWAITING_USER_ACTION: 6,
  DOWNLOADING_PACKAGE: 7,
  INSTALLING_UPDATE: 8
}

2. 当启动就选择热更新的状态变化

也就是在程序的入口就选择检查热更新,并且当有更新的时候立马安装更新,代码如下:

useEffect(() => {
  CodePush.sync(
    {
      installMode: CodePush.InstallMode.IMMEDIATE,
    },
    // 这个是在检查更新或者安装中等状态发生变化的时候会毁掉
    codePushStatusDidChange,
  )
}, [])

2.1 当没有更新的时候

当我们并没有热更新的时候, app 打开的时候的状态变化。

  1. 5(SyncStatus.CHECKING_FOR_UPDATE])也就是检查更新;
  2. 0(SyncStatus.UP_TO_DATE)app 目前运行就是最新的版本。

2.2 当有更新的时候

当我们推送了热更新以后, app 打开的时候的状态变化。

  1. 5(SyncStatus.CHECKING_FOR_UPDATE)也就是检查更新;
  2. 7(SyncStatus.DOWNLOADING_PACKAGE)下载最新的热更新包;
  3. 8(SyncStatus.INSTALLING_UPDATE)正在安装最新的热更新包;
  4. 1(SyncStatus.UPDATE_INSTALLED)最新的热更新包安装成功。
    这个时候 app 会重新加载最新的热更新包,然后会重新调用热更新检查相关逻辑。所以后面的状态变化跟 2.1 想通。有时候并没有状态 1 ,只有 5 7 8 ,因为一般安装完成后就会立马重启 app ,可能此时最后的状态 1 就会被扔掉。

2.3 当没有更新,但是网络有问题的情况

当我们并没有热更新的时候,但是此时 app 并没有网络,我们来看看状态的变化。

  1. 5(SyncStatus.CHECKING_FOR_UPDATE)也就是检查更新;
  2. 3(SyncStatus.UNKNOWN_ERROR)出现了未知问题
    我是通过设置 ios 的网络为百分之百丢包来测试的;这个过程很慢要等很久才会出现状态 3 。当我采用将手机开启飞行模式的时候,这个过程是很快的。

2.4 当没有更新,网络很慢的时候

当用户所处的网络环境不是特别好的时候,我们启动 app 的状态变化。下面列出各种网络情况下的测试结论。

  • Very Bad Network2.1 相同,只不过这个过程会比较慢;
  • 3G 同上,相比上面要快一些;
  • LTE 同上。
    其余的就不列出了,都是一样的,只是快慢的问题。

2.5 当有更新,网络很慢的时候

当我们有热更的时候,在网络环境很差的情况下启动 app 。跟 2.2 相同,只不过会慢一些。

2.6 当新增的热更新有闪退

这里说的闪退是入口闪退,也就是已加载 bundle 就闪退,而不是某个界面有问题。只要是 bundle 包本身有 bug ,都是能成功热更新的,即便是在 App 中就出现了闪退,也不会触发 code-push 的回滚机制。这里可能需要我对热更新了解更深入才能更准确,将来学习这部分安排上。
我是直接写在下面的地方的:

const syncImmediate = useCallback(() => {
    CodePush.sync(
      {
        installMode: CodePush.InstallMode.IMMEDIATE,
      },
      codePushStatusDidChange,
    )
    throw new Error('出现了错误')
  }, [codePushStatusDidChange, dispatch])

那怎样才能触发 code-push 的回滚机制呢,接下来我会进行各种测试。我看了看网络上别人写关于 code-push 的教程和说明,我看他们是写在 App.ts 中的生命周期函数里就可以做到回退,由于我是 Hooks ,所以我写到 useEffect 函数中是不行的,后来我只是写在 index.js 中就实现了回滚:

AppRegistry.registerComponent(appName, () => render)
throw new Error('test')

可以查看更新结果:

在这之前启动 app,状态跟 2.2 一样,只不过紧接着就闪退了,再次启动就是跟 2.1 相同了。然后杀死 app 再次重启,此时的状态也是跟 2.1 相同。假如说是因为其他什么原因导致的热更新失败,比如下载不完整等等,这样 app 下次再次启动的时候就重新检查然后重新安装,这时我们怎么做呢?

3. 回滚后的操作

当下载下来的包有问题,也就是在刚开始就闪退( index.js 代码中有问题),那么就会触发回滚,就像我上面测试的一样,假如我们的热更新并不是自己代码的问题导致的,那么怎么样才能恢复更新呢,因为我们知道只要重新下载再安装大概率就不会有问题了。

让我们翻开API文档,会看到这么一个参数 rollbackRetryOptions ,这个就是回滚的相关参数设置。我设置如下:

CodePush.sync(
  {
    installMode: CodePush.InstallMode.IMMEDIATE,
    rollbackRetryOptions: {
      // 方便测试我这里写的一分钟
      delayInHours: 1 / 60,
    },
  },
  codePushStatusDidChange,
)

除了 delayInHours 这个参数还有重试次数:

export interface RollbackRetryOptions {
    /**
     * Specifies the minimum time in hours that the app will wait after the latest rollback
     * before attempting to reinstall same rolled-back package. Defaults to `24`.
     */
    delayInHours?: number;

    /**
     * Specifies the maximum number of retry attempts that the app can make before it stops trying.
     * Cannot be less than `1`. Defaults to `1`.
     */
    maxRetryAttempts?: number;
}

这下等我热更新自动回滚以后,只需要再等一分钟就会再次重试。

4. 和闪屏页结合使用

假如我们热更新跟闪屏页是一起使用的,也就是当热更新完成,闪屏页才消失;那么我只需要这样即可:

CodePush.sync(
  {
    installMode: CodePush.InstallMode.IMMEDIATE,
    rollbackRetryOptions: {
      delayInHours: 8,
    },
  },
  syncStatus => {
    switch (syncStatus) {
      case CodePush.SyncStatus.UP_TO_DATE:
      case CodePush.SyncStatus.UNKNOWN_ERROR:
        SplashScreen.hideAsync() // 隐藏启动屏
        break
    }
  },
)

也就是当热更新的状态是UP_TO_DATEUNKNOWN_ERROR的时候;成功的情况下:

  1. 没有热更新,最后是UP_TO_DATE的状态;
  2. 有热更新,由于热更新成功以后再次启动也会经历跟 1 相同的状态变更,所以最终还是UP_TO_DATE的状态;
  3. 当更新失败,状态就会变成UNKNOWN_ERROR的状态。

5. 手动控制更新

可能有时候我们并不想自动更新,自动重启,那么我们首先就要修改安装模式:

CodePush.sync({installMode: CodePush.InstallMode.ON_NEXT_RESTART})

这样就不会下载完成就立即重新启动app了。当然从这个安装模式我们也能知道,即便现在不重启,如果用户手动杀死 app ,重新打开,那么也会更新的。热更新说白了就是倾向于静默更新的,只是像我们整个项目只有一个bundle的情况下,立即生效就得重启 app ,所以才需要用到这个。先看看整个热更新的流程:

我们知道只要有热更新就会下载,就会替换,替换这个过程是自动的,不能手动操作;所以不管我们的安装模式是怎样的,只要重启就会使用最新的。在这个流程中能够控制的就是重启的时间,我们可以让用户手动重启,这里说的手动重启是在 app 已经在运行的状态下进行。下面来看我编写的代码:

const checkUpdate = () => {
  // 开始检查更新
  setCheckUpdating(true)
  // 不允许重启
  CodePush.disallowRestart()
  CodePush.sync(
    {
      // 更新模式是下一次重启
      installMode: CodePush.InstallMode.ON_NEXT_RESTART,
      rollbackRetryOptions: {
        delayInHours: 1 / 3600,
      },
    },
    status => {
      // 正在下载热更新包
      if (status === CodePush.SyncStatus.DOWNLOADING_PACKAGE) {
        setCheckUpdating(false)
        setDownloadProgress('100%')
      // 开始检查更新会先触发这个状态
      } else if (status === CodePush.SyncStatus.UP_TO_DATE) {
        setCheckUpdating(false)
        setDownloadProgress(null)
        SimpleToast.show('Already the latest version.')
      // 安装包已经下载完成
      } else if (status === CodePush.SyncStatus.UPDATE_INSTALLED) {
        setCheckUpdating(false)
        setDownloadProgress(null)
        // 这是我 app 里面的弹框
        showOKCancelAlert({
          titleKey: 'Update Ready',
          messageKey:
            'Now that the update is ready, do you want to update now?',
        }).then(isOk => {
          if (isOk) {
            // 当用户点击确定就会重启
            CodePush.allowRestart()
            CodePush.restartApp(true)
          }
        })
      }
    },
    // 当下载过程中进度变化的时候会触发这个回调
    ({ totalBytes, receivedBytes }) => {
      const progress = (100 - (receivedBytes / totalBytes) * 100).toFixed(0)
      setDownloadProgress(`${progress}%`)
    },
  )
}

这样就可以实现用户手动检查,然后手动更新了。