# Code Push 热更新

它能绕过应用市场审核,实现快速多次迭代和问题修复。更准确得说是 JavaScript 能够被 Android 和 iOS 的动态执行的支持,所以才会产生替换执行代码的热更新技术。目前微软已经将其统一个到 App center 中来实现多端的更新管理。而对于我们而言只需其中的混合应用更新模块。

# 热更新流程

  1. 配置 CodePush:

    • 在项目中安装 react-native-code-push 依赖,并链接到项目中。
    • 对于 Android,需要在 android/settings.gradleandroid/app/build.gradle 中添加相关配置,并在 MainApplication.java 中初始化 CodePush。
    • 对于 iOS,需要在 AppDelegate.m 中指定 CodePush 的 bundle URL,并在 Info.plist 中添加 CodePush 的部署密钥。
  2. 发布更新:

    • 使用 code-push release-react 命令将 React Native 项目的 JavaScript bundle 和资源文件上传到 CodePush 服务器,创建一个新的更新。
    • 可以指定平台、部署环境(如 Staging 或 Production)、版本号、描述等参数。
  3. 客户端更新逻辑:

    • 应用启动或通过 API 调用时,CodePush 会检查服务器上是否有可用的更新。
    • 如果有更新,根据配置自动或提示用户下载更新包。
    • 下载完成后,根据设置的安装模式(如静默安装或立即安装),应用更新。
  4. 应用更新:

    • 用户重启应用后,如果设置了静默更新,新的 JS bundle 将被加载,应用更新内容将被应用。
    • 如果更新是强制性的,用户可能需要立即重启应用以应用更新。
  5. 更新回滚:

    • 如果更新失败或用户选择回滚,CodePush 支持回滚到上一个稳定版本。
  6. 调试和监控:

    • CodePush 提供了调试工具和 API,以便开发者可以监控更新的状态和分发效果。

# 热更新的底层原理

热更新的核心思想是 动态加载 JavaScript 代码,使得在应用发布后,开发者可以更新应用的 JavaScript 代码,而无需重新发布原生代码。React Native 和类似的框架通过这一机制实现应用的即时更新。

关键组件:

  • JavaScript 代码:打包成 bundle 文件,包含了应用的业务逻辑和 UI 渲染等内容,热更新主要是更新这部分代码。
  • 原生代码:如 Android 的 Java 和 iOS 的 Objective-C/Swift,通常不参与热更新。

热更新的关键是 动态加载和替换 JavaScript 代码,而原生代码保持不变。

# React Native 热更新的底层原理

React Native 框架通过 桥接(Bridge)技术,将 JavaScript 层与原生层连接起来,允许 JavaScript 控制原生模块和 UI 组件。

  1. React Native 框架中的底层技术

    • JavaScript 引擎:React Native 使用 JSC(JavaScriptCore)或者 Hermes 作为 JavaScript 引擎来执行 JavaScript 代码。
    • Bridge(桥接):JavaScript 和原生代码之间的通信是通过 Bridge 完成的。
    • Module System:React Native 提供了原生模块和 JavaScript 模块交互的机制,通过这些模块,JavaScript 可以调用原生代码的功能。
    • Metro Bundler:用于将 JavaScript 代码打包成一个 .bundle 文件,应用启动时加载该文件。
  2. 热更新的流程

    • 客户端启动:应用启动时,React Native 会初始化原生模块,并启动 JavaScript 引擎,加载本地或远程的 JavaScript bundle 文件。
    • 检查更新:在启动时或恢复到前台时,应用会向远程服务器请求检查是否有新的 JavaScript 更新。
    • 下载更新:如果检测到有新版本的 bundle,客户端会下载新的 JavaScript 代码包,并将其保存在本地。
    • 替换本地 bundle:下载完成后,客户端用新的 bundle 替换旧的 JavaScript 代码文件。
    • 热更新应用:新的 bundle 文件加载后,React Native 引擎执行新代码,更新应用的 UI 或业务逻辑。
    • 回滚机制:如果新 bundle 导致崩溃或错误,系统会回滚到上一个稳定版本。

# 底层实现机制

  1. JavaScript bundle 加载与替换

    • 应用启动时,加载原生模块并启动 JavaScript 引擎(JSC 或 Hermes),加载本地或远程的 JavaScript bundle 文件。
    • 当热更新检查到新的版本时,应用会异步下载并替换本地缓存的 bundle 文件。
    • 替换后,React Native 引擎执行新代码,立即更新应用的逻辑。
  2. 原生与 JavaScript 层的桥接

    • JavaScript 层通过桥接发送消息请求原生代码进行操作,原生代码通过回调返回结果给 JavaScript 层。
    • 原生代码和 JavaScript 代码之间通过消息传递机制进行通信,消息的内容通过异步队列传递,确保线程间的通信。
    • 热更新不会影响原生代码的执行,因为热更新只替换 JavaScript 层的代码。
  3. 增量更新与差分包

    • 热更新系统(如 CodePush)支持增量更新,通过比较当前 bundle 和新 bundle 的差异,生成差分包(diff patch)。
    • 客户端只需要下载差分包,而不是完整的 bundle 文件,从而减少了更新的大小和下载时间。
  4. 回滚机制

    • 热更新系统通过 崩溃监控错误日志 监测更新后的应用状态。
    • 如果新的 bundle 导致崩溃或错误,系统会自动回滚到旧版本的 bundle,确保应用的正常运行。

# 热更新的优缺点

优点

  • 快速修复 bug:开发者可以快速修复应用中的 bug,而无需提交新版本到应用商店。
  • 节省发布周期:避免频繁的提交审核和等待时间,加快应用迭代速度。
  • 即时发布:开发者可以实时发布更新,提高用户体验。

缺点

  • 不能更新原生代码:热更新仅限于 JavaScript 层,无法修改原生代码。如果需要更改原生模块或新增功能,仍然需要重新发布新版本应用。
  • 版本兼容性:热更新可能会导致某些老版本的设备无法兼容最新的 JavaScript 代码。
  • 平台限制:某些平台(特别是 Apple 的 App Store)对热更新有严格限制,可能拒绝通过热更新方式进行频繁更新。

React Native 的热更新原理依赖于动态加载和替换 JavaScript 代码。通过 Bridge 技术,JavaScript 层和原生代码之间进行通信。热更新主要是通过 异步加载新 bundle替换旧 bundle 实现的。增量更新和差分包的应用使得热更新更加高效,而回滚机制则保证了更新的可靠性。在实现过程中,热更新系统不仅保证了即时更新的能力,还需要考虑到崩溃回滚和版本兼容性等问题。

# 架构

除了自建热更新服务外,可以使用

  • CodePush:由微软开发,支持 iOS 和 Android 平台。CodePush 允许开发者通过服务器推送更新,用户无需通过应用商店即可获取更新。CodePush 使用差分更新,只下载变更的部分,节省带宽。
  • DCloud:提供一套完整的热更新解决方案,支持 uni-app、React Native、Weex 等多种框架。
  • Shorebird: 一款新兴的热更新平台,支持 React Native 和 Flutter,致力于提供快速、无缝的热更新服务。Shorebird 强调在开发和发布过程中减少开发者的工作量,并支持增量更新。
  • Expo OTA 更新:你可以使用 OTA 更新技术,例如使用 Expo 来为你的 React Native 应用提供 OTA 更新。Expo 支持通过服务器推送新的 JavaScript 包和资源,而无需发布应用商店版本。
  • 动态模块加载:如果你希望更灵活地加载新功能,可以通过实现模块化架构来加载不同的功能模块。利用 React Native 的代码拆分和动态加载机制,可以在运行时按需加载最新的功能模块。
  • 小程序容器技术:可以将应用的各个部分封装成小程序,实现热更新。
  • WebView 动态加载:如果新功能是基于 Web 的,你可以使用 WebView 来加载 Web 内容,这样你可以更新 Web 内容而无需更新 React Native 应用本身。

rn-code-push1

它由三个部件组成:

  • code-push-server 服务端。提供身份认证、更新包存储、更新校验、更新包下载、统计等等服务;负责存储应用信息、部署信息、版本信息以及更新包的具体内容。
  • code-push-cli 命令行。提供登录、代码打包、更新包部署等功能;允许开发者在本地生成更新包并推送到服务器。
  • react-native-code-push 客户端 SDK。校验更新、下载安装更新包、更新上报等功能。集成在应用中,使得应用能够检查更新、下载并应用这些更新。

# 服务端表设计

  • apps:存储应用信息,每个平台(Android、iOS、Windows)的包需要分别创建一个应用来发布更新包。
  • deployments:存储部署信息,包括访问凭证、部署环境(如 Staging 和 Production)和最后一次部署的版本 ID。
  • deployments_versions:存储发布更新包的版本信息,记录可被更新的原生端版本号范围。
  • packages:存储更新包的信息,包括下载地址、hash、是否强制更新、标签、发布者 ID 等。
  • packages_diff:存储 diff 更新包的信息。

# 身份认证及权限管理

  • Code Push 有自己独立的身份认证模块,通过 usersuser_tokenscollaborators 表来管理用户信息、登录状态和用户角色(ownercollaborator)。

# 版本策略

  • Code Push 使用 semver 语义化的版本规范,允许使用逻辑操作符约束版本号,以指定二进制版本范围所更新。
  • 版本号范围表达式的例子包括 1.2.3*1.2.*1.2.3-1.2.7>=1.2.3<1.2.7~1.2.3^1.2.3 等。

# 增量更新

  • 增量更新(差异更新)是热更新的核心功能,通过比较当前发布的全量包和最近几次发布的全量包生成 diff 包,减少用户更新时间和流量。
  • Code Push Server 端默认对最近 3 个版本的全量包进行合并,生成的 diff 包信息由 packages_diff 表记录。

# 回滚机制

  • Code Push 支持回滚到上一个版本或指定版本。
  • 回滚在服务器端实现时,会创建一条新的 packages 表记录,通过字段 origin_label 指向回滚的更新包,其 release_method 字段被标记为 Rollback

# 增量更新

增量更新(差异更新)是热更新的核心功能,通过比较当前发布的全量包和最近几次发布的全量包生成 diff 包,减少用户更新时间和流量。

它的基本工作原理是把当前发布的全量包和最近几次发布的全量包进行比对,生成 diff 包。如果用户更新命中当前发版的包版本,则只需要下载 diff 包即可。下载完 diff 包成后,在原生端本地当前版本的包和 diff 包合并成新的本地全量包,最后进行新版本包的安装。

在新包发布时,Code Push Server 端默认对最近 3 个版本的全量包进行合并。生成的 diff 包信息,由 packages_diff 表来记录。因此,一次版本的发布,对于服务器而言会存储 1 个全量包和 3 个差异包,过多版本进行 diff,会加大服务器存储空间。对于超过 3 个迭代的版本原生端,它会接收到的全量包的更新。

shm-open/code-push-server 中,你可以在 config.ts 里的 diffNums 配置来修改此设置

rn-code-push2

内置包和部署到服务器的更新包必须保持一样。否则,热更新会因为 hash 不匹配而进行全量更新

# 更新策略

# 冷启动(Cold Start)

  • 定义:冷启动是指应用在完全关闭后重新启动时进行更新。也就是当应用被完全关闭或杀掉后,重新启动时检查是否有新的更新。
  • 工作原理
    • 在应用启动时,React Native 会首先检查远程是否有新的 JavaScript bundle,如果有,客户端会下载新的 bundle 并替换本地缓存的版本。
    • 更新后的 bundle 会立即在应用启动时加载,并覆盖旧的代码逻辑。
  • 适用场景:适用于应用第一次启动时进行更新,通常是在没有运行中的应用实例时。
  • 优点
    • 更新流程简单,用户在启动应用时可以获得最新版本。
    • 更新时不会影响用户的使用体验,因为更新是在应用启动时完成的。
  • 缺点
    • 需要用户完全退出应用并重新启动,更新无法在应用已经运行时生效。
    • 如果用户没有重新启动应用,更新可能会被错过。

# 热启动(Hot Start)

  • 定义:热启动指应用在后台运行时进行更新,下载更新包并在用户切换到前台时应用新的 bundle 文件。
  • 工作原理
    • 当应用在后台时,React Native 客户端会在后台悄悄下载新的 bundle 文件,并在下次用户打开应用时替换旧版本的 bundle 文件。
    • 用户进入应用的前台时,应用会加载并执行新的 JavaScript 代码。
  • 适用场景:适用于更新操作无需干扰用户使用应用的场景。
  • 优点
    • 用户在下次打开应用时可以看到更新的内容,减少了用户等待的时间。
    • 更新过程不会打扰用户,尤其是当用户切换到其他应用或使用多任务时。
  • 缺点
    • 需要确保更新在后台顺利下载,且不会影响应用性能。
    • 更新效果会在用户恢复应用时才生效,可能存在延迟。

# 前台更新(Foreground Update)

  • 定义:前台更新是指当用户打开应用并处于前台时,应用会检查是否有新的更新,并提示用户下载更新或者自动更新。
  • 工作原理
    • 当应用处于前台时,React Native 客户端会立即检查是否有新版本的 bundle,如果有,会下载并替换当前 bundle 文件。
    • 一旦更新完成,应用会重新加载新的 JavaScript 代码并生效。
  • 适用场景:适用于希望在用户活跃时立即应用更新的场景。
  • 优点
    • 可以确保用户在使用时获得最新的功能和 bug 修复。
    • 更新过程在用户使用时就会开始生效。
  • 缺点
    • 可能会打断用户的使用体验,尤其是更新内容较大时。
    • 需要较长时间的下载和更新,可能导致较长的等待时间。

# 后台更新(Background Update)

  • 定义:后台更新是指应用在后台运行时,客户端会定期检查是否有新的更新,下载更新包并在下一次应用启动时进行更新。
  • 工作原理
    • 应用在后台运行时,React Native 客户端会持续或定时检查是否有更新。如果有,客户端会在后台静默下载更新包。
    • 更新会在应用下次启动时生效,下载的 bundle 会在应用启动时被加载并替换原有的 bundle 文件。
  • 适用场景:适用于希望应用在后台静默更新,不影响当前用户使用的场景。
  • 优点
    • 用户使用应用时无需干预,更新过程不会影响体验。
    • 更新操作是在后台进行,避免了用户感知。
  • 缺点
    • 下载更新包时,可能会占用网络带宽,影响其他应用的网络请求。
    • 如果下载过程失败,可能导致下一次应用启动时无法应用新版本。

# 触发式更新(On-demand Update)

  • 定义:触发式更新是指应用根据特定条件或用户操作来触发更新,如用户点击“检查更新”按钮或在某些条件满足时更新。
  • 工作原理
    • 只有当用户主动触发更新请求时,客户端才会检查并下载更新。
    • 这种方式通常与手动触发更新的按钮或者定时检查等功能配合使用。
  • 适用场景:适用于需要用户主动决定是否更新的场景,特别是当更新内容不紧急时。
  • 优点
    • 用户可以自由选择是否进行更新,更新过程完全可控。
    • 适用于不需要频繁更新的应用。
  • 缺点
    • 用户可能会错过某些重要更新,尤其是在不主动检查更新的情况下。
    • 更新不是自动完成的,用户体验相对较差。

# 强制更新(Forced Update)

  • 定义:强制更新是指应用强制要求用户更新到最新版本,用户无法继续使用旧版本。
  • 工作原理
    • 当应用检测到有新版本时,强制要求用户更新。应用会通过弹出提示框或其他方式提醒用户进行更新,直到用户完成更新后才能继续使用。
  • 适用场景:适用于紧急修复 bug、安全漏洞或者在新版本中进行重要改动的情况。
  • 优点
    • 确保所有用户都更新到最新版本,避免使用过时版本的风险。
    • 适用于重要的安全更新或功能修复。
  • 缺点
    • 可能导致用户不满,尤其是在他们不希望立即更新时。
    • 强制更新可能中断用户的体验,尤其是当更新需要较长时间时。

# 渐进式更新(Progressive Update)

  • 定义:渐进式更新是指通过分批次更新的方式,将更新内容分成多个小块逐步下载和应用。
  • 工作原理
    • 应用会分阶段下载和应用更新,而不是一次性下载所有内容,避免大规模的更新包导致应用启动时间过长。
    • 每次应用启动时,都会下载并应用其中的一部分更新,直到所有更新完成。
  • 适用场景:适用于大型应用或更新内容非常多的情况,分批次进行更新可以避免一次性更新带来的问题。
  • 优点
    • 用户在更新过程中无需等待所有内容下载完成,提升了用户体验。
    • 可以减轻大规模更新时的网络压力。
  • 缺点
    • 更新过程需要分阶段完成,可能导致用户在不同阶段体验到不同版本的应用。
    • 增加了更新管理的复杂性。

React Native 提供了多种热更新策略,每种策略都适用于不同的需求和使用场景。冷启动(Cold Start)是最常见的更新策略,但根据应用的需求,开发者可以选择更合适的更新方式,如热启动、前台更新、后台更新、强制更新等。每种策略都有其优缺点,需要根据用户体验、网络条件和更新频率来选择最合适的策略。

  1. 冷启动(Cold Start):更新在应用完全关闭后重新启动时进行。优点是更新简单,用户不会感知到更新过程;缺点是需要用户重启应用,更新可能会被错过。

  2. 热启动(Hot Start):应用在后台运行时下载更新,切换到前台时应用新更新。优点是无需重启应用,更新过程不会打扰用户;缺点是更新效果在用户进入前台时才生效,可能有延迟。

  3. 前台更新(Foreground Update):应用在前台时检查更新,并立即下载和应用。优点是用户实时体验最新版本;缺点是可能中断用户体验,尤其更新较大时。

  4. 后台更新(Background Update):应用在后台时自动下载更新,下一次启动时生效。优点是不会影响用户体验,静默更新;缺点是下载过程可能影响其他网络请求,且下载失败可能导致无法应用新版本。

  5. 触发式更新(On-demand Update):用户主动触发更新,例如点击“检查更新”按钮。优点是更新可控,用户可选择是否更新;缺点是用户可能错过重要更新,依赖用户主动行为。

  6. 强制更新(Forced Update):强制要求用户更新到最新版本,无法继续使用旧版本。优点是确保用户使用最新版本,适用于重大修复或安全更新;缺点是可能引发用户不满,打断使用体验。

  7. 渐进式更新(Progressive Update):将更新分批下载和应用,逐步完成更新。优点是避免大更新包导致的性能问题,减轻网络压力;缺点是更新分阶段进行,可能导致不同版本的应用在不同时间段存在。