App 热更新(OTA)原理深解 —— 以 React Native 为例

热更新让你绕过应用商店的审核,直接把新逻辑推送给用户。本文从架构原理、推送流程、关键代码到生产注意事项,完整梳理一遍。


一、为什么 React Native 能热更新

React Native 应用由两层组成:Native 壳 (Java / OC / Swift 编译的原生代码)和 JS Bundle(真正的业务逻辑)。Native 壳负责渲染和桥接系统 API,JS Bundle 负责组件逻辑、路由、状态管理------几乎全部业务代码都在这里。

换句话说:换 bundle 就等于换 App 逻辑,用户不需要重新安装 App。这就是热更新的本质。

⚠️ 关键限制 :热更新只能更新 JS bundle 和图片等资源文件,不能修改 Native 代码(如新增原生模块、修改 Manifest 权限)。涉及 Native 变更必须走完整发版流程。


二、完整推送流程

从开发者改代码到用户看到新版本,整个链路分为三个阶段:打包上传、启动检查、下载生效。

flowchart TD subgraph P1["阶段一:打包 & 上传"] A["👨‍💻 开发者\n修改 JS / 资源文件"] B["📦 JS Bundle\nindex.android.bundle"] C["🖥️ 更新服务器\n存储 bundle + 版本号"] D[/"☁️ CDN\n分发加速"/] A -- "react-native bundle" --> B B -- "上传 + 版本号" --> C C -. "同步" .-> D end subgraph P2["阶段二:App 启动检查"] E["📱 App 启动\n读取本地版本号"] F["🖥️ 更新服务器\n返回最新版本号"] G{"版本比较\n本地 vs 远端"} H["✅ 使用本地 bundle\n无需更新"] E -- "GET /check?appVersion=&currentBundle=" --> F F --> G G -- "无更新" --> H end subgraph P3["阶段三:下载 & 生效"] I["⬇️ 下载新 bundle\n从 CDN 获取 zip"] J["💾 写入本地存储\n校验 MD5 / 解压"] K["🔄 重新加载 bundle\nJS 引擎重新执行"] L["🎉 更新生效\n用户看到新版本 UI"] M[("本地持久化\n版本号 + bundle 路径")] I --> J --> K --> L L -. "保存版本号" .-> M end P1 --> P2 G -- "有新版本" --> I

💡 bundle 文件本身走 CDN 下发,而不是应用服务器------bundle 通常几 MB 到十几 MB,CDN 更快更省钱。


三、后端接口设计

后端核心是一个版本检查接口,App 启动时携带当前信息请求,服务端决定是否推送及推哪个包。

请求参数

ini 复制代码
GET /api/ota/check
  ?appVersion=1.2.0       // Native 壳版本(很重要!)
  &platform=android       // android / ios / harmony
  &channel=production     // 灰度分组
  &currentBundle=20240801 // 当前 bundle 版本号

响应结构

json 复制代码
{
  "hasUpdate": true,
  "bundleVersion": "20240810",
  "bundleUrl": "https://cdn.example.com/bundles/android_20240810.zip",
  "md5": "a1b2c3d4e5f6...",        // 完整性校验
  "mandatory": false,               // 是否强制立即更新
  "description": "修复搜索崩溃问题" // 更新说明(可选)
}

⚠️ appVersion 字段至关重要------如果新 bundle 调用了新的 Native API,但用户 App 还是旧版 Native 壳,就会直接 crash。服务端必须按 appVersion 分流,只给兼容的 Native 版本推对应的 bundle。


四、客户端核心逻辑

以下是自建 OTA 的客户端核心流程,使用 CodePush 或 pushy 的项目原理相同,只是封装了这些步骤。

javascript 复制代码
import { MMKV } from 'react-native-mmkv';

const storage = new MMKV();

async function checkOTAUpdate() {
  // 1. 读取当前本地版本号
  const currentBundle = storage.getString('bundleVersion') ?? 'base';
  const appVersion = DeviceInfo.getVersion();

  // 2. 请求检查接口
  const res = await fetch(
    `https://ota.example.com/api/check?appVersion=${appVersion}&currentBundle=${currentBundle}`
  );
  const { hasUpdate, bundleUrl, bundleVersion, md5, mandatory } = await res.json();

  if (!hasUpdate) return;

  // 3. 后台下载新 bundle
  const localPath = await downloadBundle(bundleUrl);

  // 4. MD5 校验(防包损坏 / 防中间人篡改)
  const valid = await verifyMD5(localPath, md5);
  if (!valid) {
    console.warn('OTA: MD5 mismatch, aborting');
    return;
  }

  // 5. 替换 bundle,持久化路径和版本号
  await replaceBundle(localPath);
  storage.set('bundleVersion', bundleVersion);

  // 6. 生效策略:强制立即重启 or 等下次冷启动
  if (mandatory) {
    RNRestart.Restart(); // 立即重载
  }
  // 非强制:下次启动自动用新 bundle
}

五、生产环境必须处理的问题

① 灰度发布

服务端在 /check 接口中,根据用户 ID 或设备 ID 做分桶,先推送 1%,监控崩溃率和 ANR,稳定后逐步扩量到 10% → 50% → 100%。这套逻辑完全在后端控制,客户端无需感知。

② 回滚机制

本地需保留最近 2~3 个历史 bundle(不能只存最新的)。当新 bundle 首次加载后崩溃率超过阈值,客户端自动回退到上一个版本,并上报异常告警后端关闭该批次推送。

③ 差量更新(bsdiff)

全量 bundle 通常几 MB 到十几 MB,用户流量成本高。成熟的 OTA 系统会使用 bsdiff 算法,只下发新旧 bundle 的差异 patch,客户端本地合并,包大小可降至几十 KB。

④ Native 版本兼容矩阵

维护一张 native_version → compatible_bundle_versions 的映射表,确保下发给 Native 1.0 壳的 bundle 不会调用只有 Native 1.1 才有的 API。


六、主流方案对比

方案 提供方 适合场景 主要限制
CodePush Microsoft (App Center) 快速接入,国际项目 国内访问速度差,免费额度有限
pushy React Native 中文网 国内项目,合规友好 依赖第三方服务,定制空间有限
自建 OTA 服务 自研 大厂、高定制、灰度精细化 开发维护成本高

⚠️ HarmonyOS / RNOH 注意 :RNOH 目前对 CodePush 的支持不完整。HarmonyOS 平台做热更新需要关注华为应用市场的合规要求,以及 RNOH bundle 格式是否与 Android/iOS 完全一致------建议在自建 OTA 服务中按 platform 字段分别管理 bundle。


七、小结

  • 热更新的本质是替换 JS bundle,Native 层不动
  • 后端核心是版本检查接口 + CDN 分发,bundle 文件本身走 CDN 而非应用服务器
  • 生产环境必须有灰度、回滚、MD5 校验三件套
  • appVersion(Native 版本)要传给服务端,避免 bundle 与 Native API 不兼容导致 crash
  • 国内项目推荐 pushy 或自建;HarmonyOS 需单独处理平台兼容
相关推荐
人民广场吃泡面3 小时前
前端该重视的编程思想
前端
冬夜戏雪3 小时前
agent的trace 和eval
开发语言·前端·javascript
IT_陈寒3 小时前
Redis的DEL命令居然没删干净数据?这个坑我爬了半天
前端·人工智能·后端
kyriewen3 小时前
面试官让我现场用 AI 改一个真实 bug——他打断我的 3 个理由
前端·面试·ai编程
计算机魔术师4 小时前
斯坦福研究:AI 对入门级岗位冲击最大
前端
Simon_He5 小时前
一个渲染内核,五个框架包:我的流式 Markdown 渲染库是怎么长成“全家桶”的
前端·vue.js·markdown
小满zs5 小时前
关于HBuilderX+ 微信小程序开发工具启动的问题
前端·uni-app
林深见鹿_海蓝见鲸5 小时前
微信小程序反编译完整教程(Windows 新手版)
前端