文章

热更新原理深度解析

从CodePush实现到Bundle分包策略,全面拆解React Native热更新的核心技术原理与实践方案

热更新原理深度解析

一句话概括

React Native热更新的核心是bundle文件的远程下载、版本差异化替换和运行时代码注入,涉及增量补丁算法、Assets资源管理、Rollback回滚机制等关键技术。

背景与意义

为什么RN需要热更新?

App Store和各大安卓应用市场的审核周期决定了一次发版至少需要1-2天(紧急审核最快也要几小时)。对于追求敏捷迭代的团队来说,这个周期完全不可接受。

竞品对比数据(某电商App实际数据):

场景原生发版RN热更新差异
修复空指针闪退2-12小时10分钟72x
调整弹窗文案8-24小时5分钟288x
上线新促销页面24-72小时1小时72x
灰度AB实验不支持支持-

热更新的技术边界

热更新不是万能的,它有明确的能力范围

1
2
3
4
5
6
7
8
9
10
11
12
13
热更新能做的:
├── JS Bundle文件替换 ✓
├── 图片/字体等App资源更新 ✓
├── JS代码逻辑变更 ✓
├── 样式和布局调整 ✓
└── 第三方JS库版本更新 ✓

热更新不能做的:
├── 原生代码(Java/ObjC/Swift)修改 ✗
├── Info.plist / AndroidManifest.xml 变更 ✗
├── 新增原生Module ✗
├── 新增系统权限 ✗
└── 动态库/静态库替换 ✗

概念与定义

热更新标准流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 开发阶段
    ├── JS代码编写
    ├── Metro打包 → bundle文件
    └── 上传到更新服务器(CodePush / 自建CDN)

2. 客户端启动
    ├── 检查本地版本号
    ├── 请求服务器查更新
    ├── 下载差异包/全量包
    └── 校验完整性(MD5/hash)

3. 运行时切换
    ├── 替换本地bundle文件
    ├── 下次冷启动加载新bundle
    └── 或使用 enableHotReload 技术实时切换

核心术语

术语含义
BundleMetro打包输出的单一JS文件,包含所有业务代码+依赖
BundleHashbundle文件的SHA256哈希值,用于版本唯一标识
Deployment KeyCodePush中环境区分标识(Staging/Production)
Rollback升级失败后自动降级到上一版本
Mandatory强制更新标记,标记为true时会强制用户升级
Diff Patch基于bsdiff/hdiff的增量补丁包

最小示例:基于CodePush的更新流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// App.tsx - 集成CodePush的入口文件
import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  ActivityIndicator,
  StyleSheet,
  Alert,
  Platform,
} from 'react-native';
import codePush from 'react-native-code-push';

type UpdateStatus = 'checking' | 'downloading' | 'installing' | 'ready' | 'error';

const App: React.FC = () => {
  const [status, setStatus] = useState<UpdateStatus>('checking');
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    performUpdateCheck();
  }, []);

  const performUpdateCheck = async () => {
    try {
      setStatus('checking');

      // 1. 检查更新
      const remotePackage = await codePush.checkForUpdate(
        Platform.select({
          ios: 'IOS_DEPLOYMENT_KEY',
          android: 'ANDROID_DEPLOYMENT_KEY',
        })!
      );

      if (!remotePackage) {
        // 没有可用更新
        console.log('[CodePush] 当前已是最新版本');
        setStatus('ready');
        return;
      }

      console.log('[CodePush] 发现新版本:', remotePackage.appVersion);

      // 2. 下载更新包
      setStatus('downloading');
      const downloadedPackage = await remotePackage.download(
        (progressEvent) => {
          const percent = Math.round(
            (progressEvent.receivedBytes / progressEvent.totalBytes) * 100
          );
          setProgress(percent);
          console.log(`[CodePush] 下载进度: ${percent}%`);
        }
      );

      // 3. 安装更新
      setStatus('installing');
      if (remotePackage.isMandatory) {
        // 强制更新:立即安装并重启
        await downloadedPackage.install(codePush.InstallMode.IMMEDIATE);
      } else {
        // 静默更新:下次冷启动时安装
        await downloadedPackage.install(codePush.InstallMode.ON_NEXT_RESTART);
      }

      setStatus('ready');
    } catch (err: any) {
      console.error('[CodePush] 更新失败:', err);
      setStatus('error');
      setError(err.message || '未知错误');
    }
  };

  if (status === 'downloading') {
    return (
      <View style={styles.center}>
        <Text style={styles.title}>正在下载更新...</Text>
        <ActivityIndicator size="large" color="#4A90D9" />
        <Text style={styles.progress}>进度: {progress}%</Text>
      </View>
    );
  }

  if (status === 'installing') {
    return (
      <View style={styles.center}>
        <Text style={styles.title}>正在安装更新...</Text>
        <ActivityIndicator size="large" color="#4A90D9" />
      </View>
    );
  }

  if (status === 'error') {
    return (
      <View style={styles.center}>
        <Text style={styles.title}>更新检查失败</Text>
        <Text style={styles.errorText}>{error}</Text>
      </View>
    );
  }

  // 正常渲染业务页面
  return <MainScreen />;
};

// codePush高阶组件装饰
const codePushOptions = {
  checkFrequency: codePush.CheckFrequency.ON_APP_START,
  installMode: codePush.InstallMode.ON_NEXT_RESUME,
  mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
  updateDialog: {
    title: '发现新版本',
    optionalUpdateMessage: '需要更新吗?',
    mandatoryUpdateMessage: '请更新到此版本',
    optionalInstallButtonLabel: '稍后',
    mandatoryContinueButtonLabel: '立即更新',
  },
};

export default codePush(codePushOptions)(App);

// 主页面(占位)
const MainScreen: React.FC = () => (
  <View style={styles.center}>
    <Text style={styles.mainTitle}>首页</Text>
    <Text style={styles.subText}>版本: {Platform.OS} v1.0.0</Text>
  </View>
);

const styles = StyleSheet.create({
  center: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' },
  title: { fontSize: 18, fontWeight: 'bold', marginBottom: 20, color: '#333' },
  mainTitle: { fontSize: 24, fontWeight: 'bold', color: '#333' },
  subText: { fontSize: 14, color: '#999', marginTop: 8 },
  progress: { fontSize: 14, color: '#666', marginTop: 12 },
  errorText: { fontSize: 14, color: '#FF3B30', marginTop: 8 },
});

核心知识点拆解

1. Bundle的生成与结构

Metro打包器输出的是一个自执行的JS闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// xxx.bundle (Metro输出示例)
__d(function(g, r, i, a, m, e, d) {
    // 模块定义
    // g: global, r: require, i: moduleId
    var React = r(0); // 引用模块ID为0的React
    // ... 业务代码 ...
}, 42); // 模块ID为42

__d(function(g, r, i, a, m, e, d) {
    // 另一个模块
}, 0); // 模块ID为0的React

// 入口点
require(42);

Bundle的内部结构:

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────┐
│ Polyfills           │ ← Promise, Object.assign 等polyfill
├─────────────────────┤
│ React & RN Runtime  │ ← react, react-native 核心库 (固定不变)
├─────────────────────┤
│ 第三方依赖          │ ← lodash, axios 等
├─────────────────────┤
│ 业务代码            │ ← 频繁更新
├─────────────────────┤
│ Source Map          │ ← 调试用
│ Assets Manifest     │ ← 引用图片等资源
└─────────────────────┘

2. 增量更新原理

全量更新需要下载整个bundle(通常3-8MB),增量更新通过差异化算法只传输变更部分。

bsdiff算法流程:

1
2
3
4
5
6
7
旧Bundle (1.0)          新Bundle (2.0)
    │                       │
    └─────── bsdiff ────────┘
                │
        [bsdiff补丁包] ← 通常只有全量的10-20%大小
                │
    旧Bundle + 补丁 → bspatch → 新Bundle

CodePush的增量更新策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CodePush下载策略的伪代码
async function downloadUpdate(remotePackage: RemotePackage) {
  // 1. 计算本地哈希
  const localHash = await getLocalBundleHash();

  // 2. 询问服务器是否有差异补丁
  const patchUrl = `${CDN_URL}/patch/${remotePackage.packageHash}/${localHash}`;
  const patchResponse = await fetch(patchUrl);

  if (patchResponse.status === 200) {
    // 3. 下载补丁包并应用
    const patchBuffer = await patchResponse.arrayBuffer();
    const oldBundle = await readLocalBundle();
    const newBundle = applyBsPatch(oldBundle, patchBuffer);
    await writeNewBundle(newBundle);
  } else {
    // 没有差异补丁,下载全量包
    const fullBundle = await downloadFullBundle(remotePackage.downloadUrl);
    await writeNewBundle(fullBundle);
  }
}

3. Assets资源管理

热更新时,图片等资源文件需要一并处理:

1
2
3
4
5
6
7
8
9
App Bundle (原生包内的资源)
├── assets/images/splash.png
├── assets/fonts/iconfont.ttf
└── ...

热更新下载的资源
├── assets/images/banner_v2.png  ← 同名文件替换
├── assets/images/icon_new.png   ← 新增资源
└── ...

资源更新的关键问题:热更新下载的新资源存储在 App 的沙盒目录(NSDocumentDirectory / getFilesDir())中,而非App的安装包内。RN加载图片时需优先从沙盒目录查找。

1
2
3
4
5
6
7
8
9
10
11
// 图片加载器的决策逻辑
function resolveAsset(assetName: string): string {
  const sandboxPath = `${CodePush.sandboxDir}/assets/${assetName}`;
  const bundlePath = `assets://${assetName}`;

  // 优先返回沙盒路径(热更新覆盖的资源)
  if (fs.existsSync(sandboxPath)) {
    return sandboxPath;
  }
  return bundlePath;
}

4. Rollback回滚机制

CodePush的自动回滚机制是防止生产事故的最后防线:

1
2
3
4
5
6
7
8
9
10
11
升级流程:
  下载成功 → 安装 → 重启 → 启动新Bundle
                              │
                     ┌────────┴────────┐
                     ↓                  ↓
               onError 触发        App正常运行
                     │                  │
              累计3次↓                  ↓
              crash在启动30秒内      标记为成功
                     │                  │
               回滚到旧版本        保持新版本

回滚标志文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// CodePush回滚检测逻辑(简化)
function checkRollback() {
  const rollbackMarkerPath = `${sandboxDir}/codepush/crashed.json`;
  if (fs.existsSync(rollbackMarkerPath)) {
    const crashCount = JSON.parse(fs.readFileSync(rollbackMarkerPath)).count;
    if (crashCount >= 3) {
      // 三次都是crash,执行回滚
      revertToPreviousBundle();
      fs.unlinkSync(rollbackMarkerPath);
    } else {
      // 累加crash次数
      crashCount++;
      fs.writeFileSync(rollbackMarkerPath, JSON.stringify({ count: crashCount }));
    }
  }
}

5. Bundle分包策略

为什么需要分包?

单一bundle在大规模App中会膨胀到10-20MB,全量下载体验极差。分包策略将代码拆分为基础包业务包

1
2
3
4
5
6
7
8
9
10
11
bundle结构(分包后):
┌──────────────────────┐
│ 基础包 (base.bundle) │ ← 5MB, 首次下载后基本不变
│ - react              │   包含RN框架、核心依赖
│ - react-native       │
│ - 通用工具库          │
├──────────────────────┤
│ 业务包 (biz.bundle)  │ ← 1-3MB, 每次迭代更新
│ - 页面组件            │
│ - 业务逻辑            │
└──────────────────────┘

实现分包的Metro配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// metro.config.js
const { createModuleIdFactory } = require('react-native-bundle-splitter');

module.exports = {
  // ...
  serializer: {
    createModuleIdFactory: () => {
      const fileToIdMap = new Map();
      let nextId = 0;

      return (filePath) => {
        if (fileToIdMap.has(filePath)) {
          return fileToIdMap.get(filePath);
        }
        const id = nextId++;
        fileToIdMap.set(filePath, id);
        return id;
      };
    },
    // 自定义输出规则
    processModuleFilter: (modules) => {
      // 将特定目录下的模块拆分为单独bundle
      return !modules.path.includes('BusinessModules/');
    },
  },
};

实战案例:自建热更新服务

对于有安全合规要求的公司,自建热更新平台是常见选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// UpdateServer.ts - 基于Node.js的简易更新服务器
import express from 'express';
import multer from 'multer';
import crypto from 'crypto';
import fs from 'fs/promises';
import path from 'path';

const app = express();
const upload = multer({ dest: 'uploads/' });

interface VersionInfo {
  appVersion: string;
  bundleHash: string;
  downloadUrl: string;
  isMandatory: boolean;
  releaseNotes: string;
  createdAt: string;
}

const versions: Map<string, VersionInfo[]> = new Map();

// 上传新版本Bundle
app.post('/api/upload', upload.single('bundle'), async (req, res) => {
  const { appVersion, isMandatory, releaseNotes } = req.body;
  const bundleFile = req.file!;

  // 计算文件哈希
  const fileBuffer = await fs.readFile(bundleFile.path);
  const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');

  // 移动到持久化存储
  const targetPath = path.join('bundles', `${hash}.bundle`);
  await fs.rename(bundleFile.path, targetPath);

  // 记录版本信息
  const versionInfo: VersionInfo = {
    appVersion,
    bundleHash: hash,
    downloadUrl: `/api/download/${hash}`,
    isMandatory: isMandatory === 'true',
    releaseNotes: releaseNotes || '',
    createdAt: new Date().toISOString(),
  };

  if (!versions.has(appVersion)) {
    versions.set(appVersion, []);
  }
  versions.get(appVersion)!.push(versionInfo);

  res.json({ success: true, versionInfo });
});

// 客户端检查更新
app.get('/api/check-update', async (req, res) => {
  const appVersion = req.query.appVersion as string;
  const localHash = req.query.bundleHash as string;

  const appVersions = versions.get(appVersion);
  if (!appVersions || appVersions.length === 0) {
    return res.json({ updateAvailable: false });
  }

  const latest = appVersions[appVersions.length - 1];
  if (latest.bundleHash === localHash) {
    return res.json({ updateAvailable: false });
  }

  // 尝试生成差异补丁
  let patchUrl: string | null = null;
  if (localHash) {
    // 查找本地版本对应的Bundle
    const localVersion = appVersions.find(v => v.bundleHash === localHash);
    if (localVersion) {
      patchUrl = `/api/patch/${localHash}/${latest.bundleHash}`;
    }
  }

  res.json({
    updateAvailable: true,
    downloadUrl: patchUrl || latest.downloadUrl,
    bundleHash: latest.bundleHash,
    isMandatory: latest.isMandatory,
    releaseNotes: latest.releaseNotes,
    isPatch: !!patchUrl,
  });
});

// 生成差异补丁
app.get('/api/patch/:oldHash/:newHash', async (req, res) => {
  const { oldHash, newHash } = req.params;

  try {
    const oldBundlePath = path.join('bundles', `${oldHash}.bundle`);
    const newBundlePath = path.join('bundles', `${newHash}.bundle`);

    const oldBuffer = await fs.readFile(oldBundlePath);
    const newBuffer = await fs.readFile(newBundlePath);

    // 使用bsdiff生成差异
    const patchBuffer = require('bsdiff').diff(oldBuffer, newBuffer);
    res.set('Content-Type', 'application/octet-stream');
    res.send(patchBuffer);
  } catch (err) {
    // 生成补丁失败,回退到全量下载
    res.redirect(`/api/download/${newHash}`);
  }
});

app.listen(3000, () => {
  console.log('热更新服务运行在 http://localhost:3000');
});

底层原理(源码分析)

CodePush SDK核心逻辑

CodePush的更新检查与下载在Android端的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// CodePush.java (Android SDK, 简化)
public class CodePush {
    public void checkForUpdate(final Callback callback) {
        new Thread(() -> {
            try {
                // 1. 获取本地Bundle信息
                String localHash = getLocalBundleHash();
                String appVersion = getAppVersion();

                // 2. 请求更新服务器
                URL url = new URL(mUpdateEndpoint
                    + "?appVersion=" + appVersion
                    + "&deploymentKey=" + mDeploymentKey
                    + "&isFirstRun=" + isFirstRun()
                    + "&isPendingUpdate=" + isPendingUpdate());

                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                JSONObject response = new JSONObject(readStream(conn.getInputStream()));

                if (response.getBoolean("updateAvailable")) {
                    // 3. 保存更新信息
                    String downloadUrl = response.getString("downloadUrl");
                    String newHash = response.getString("appVersion");
                    boolean isMandatory = response.getBoolean("isMandatory");
                    saveUpdateInfo(downloadUrl, newHash, isMandatory);
                }

                callback.invoke(response);
            } catch (Exception e) {
                callback.invoke(null);
            }
        }).start();
    }

    private void saveUpdateInfo(String url, String hash, boolean mandatory) {
        // 写入SharedPreferences
        SharedPreferences prefs = getContext()
            .getSharedPreferences("CodePush", Context.MODE_PRIVATE);
        prefs.edit()
            .putString("downloadUrl", url)
            .putString("hash", hash)
            .putBoolean("mandatory", mandatory)
            .apply();
    }
}

JSBundleLoader的运行时切换

当热更新安装后,App重新启动时如何加载新的bundle?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// JSBundleLoader.java - RN框架源码
public abstract class JSBundleLoader {
    // 从不同来源加载bundle
    public static JSBundleLoader createAssetLoader(
        Context context, String assetUrl, boolean loadSynchronously) {
        return new JSBundleLoader() {
            @Override
            public String loadScript(JSContextHolder holder) {
                // 从Assets目录加载(原生包内)
                return loadFromAssets(context, assetUrl);
            }
        };
    }

    public static JSBundleLoader createFileLoader(String fileName) {
        return new JSBundleLoader() {
            @Override
            public String loadScript(JSContextHolder holder) {
                // 从文件系统加载(热更新下载的bundle)
                String realPath = fileName;
                if (!fileName.startsWith("/")) {
                    realPath = context.getFilesDir() + "/" + fileName;
                }
                return loadFromFile(realPath);
            }
        };
    }
}

// ReactInstanceManager构建时根据配置选择loader
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder();
if (codePushBundlePath != null) {
    // 使用CodePush替换后的路径
    builder.setJSBundleLoader(
        JSBundleLoader.createFileLoader(codePushBundlePath)
    );
} else {
    // 使用默认的Assets loader
    builder.setJSBundleLoader(
        JSBundleLoader.createAssetLoader(context, "index.android.bundle", false)
    );
}

Metro的打包优化

Metro在打包时的模块ID重排策略对bundle体积有直接影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Metro对模块ID的编码策略
// 大项目可能有5000+模块,ID从0开始顺序编号
// 对比:Webpack使用的是路径字符串作为模块标识
// Metro使用数字ID + 预定义的模块映射表

// 产物中的模块注册
__d(function(g, r, i, a, m, e, d) {
  // 模块ID: 42
  r(0); // require('react')
  r(7); // require('react-native')
  // ...
}, 42);

// 打包后的模块ID是连续整数,gzip压缩率高
// 这是RN Bundle的gzip压缩比通常能达到5:1的原因

高频面试题解析

问题1:CodePush的Mandatory和Silent更新有什么区别?实现上有什么不同?

解析

特性Mandatory(强制更新)Silent(静默更新)
用户感知弹窗提示,必须更新后台下载,无感知
安装时机立即安装并重启App下次冷启动时加载
适用场景严重Bug修复、安全更新日常功能迭代、UI调整
实现差异installMode: IMMEDIATEinstallMode: ON_NEXT_RESTART

实现差异本质:强制更新在下载完成后立即用 RCTReloadCommand 触发Reload。静默更新只将bundle路径写入SharedPreferences,下次 ReactInstanceManager 初始化时通过 JSBundleLoader.createFileLoader() 读取新路径。

问题2:热更新Bundle更新后用户数据会丢失吗?

解析

不会丢失。 原因如下:

  1. 数据分离:AsyncStorage、SQLite数据库等存储在 NSDocumentDirectory / data/data/<包名>/databases/,与bundle文件无关
  2. Bundle只替换JS代码:热更新替换的是 index.bundle 文件和Assets资源,不涉及持久化存储
  3. State恢复:RN的Redux/Flutter状态需要自行持久化(如redux-persist),但这属于业务层设计

但需要注意以下场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 危险操作:热更新后在AsyncStorage中存储了"新版本schema的数据"
// 但旧版本bundle无法解析新字段,导致用户回退后数据损坏

// ✅ 正确做法:使用版本化存储
const STORAGE_KEY = `@myapp:todos_v3`;
// 读取时检查版本兼容
async function migrateIfNeeded() {
  const version = await AsyncStorage.getItem('@myapp:schema_version');
  if (version !== '3') {
    // 执行数据迁移
    await migrateData();
    await AsyncStorage.setItem('@myapp:schema_version', '3');
  }
}

问题3:如何设计一个支持灰度发布的热更新方案?

解析

灰度发布需要在更新服务端实现条件过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 更新服务的灰度策略
interface GrayConfig {
  // 按用户ID哈希灰度(均匀分布)
  userIdHashMod: { mod: number; eq: number };
  // 按渠道灰度
  channels: string[];
  // 按地区灰度
  regions: string[];
  // 时间窗口
  timeWindow: { start: string; end: string };
}

function shouldUpdate(user: User, config: GrayConfig): boolean {
  // 1. 时间窗口校验
  const now = Date.now();
  if (now < new Date(config.timeWindow.start).getTime() ||
      now > new Date(config.timeWindow.end).getTime()) {
    return false;
  }

  // 2. 渠道匹配
  if (config.channels.length > 0 && !config.channels.includes(user.channel)) {
    return false;
  }

  // 3. 用户哈希灰度
  if (config.userIdHashMod) {
    const hash = simpleHash(user.id) % config.userIdHashMod.mod;
    return hash === config.userIdHashMod.eq;
  }

  return true;
}

// 灰度放量流程
// 阶段1(1%):验证稳定性 → 阶段2(5%):验证兼容性
// 阶段3(20%):收集性能数据 → 阶段4(100%):全量发布

总结与扩展

核心要点

  1. Bundle结构:RN热更新本质是替换JS Bundle文件,Metro打包将模块ID化使其体积可控
  2. 增量更新:通过bsdiff算法将传输量降低80-90%,是热更新的性能关键
  3. Rollback机制:启动时crash计数器的实现是热更新安全的最后保障
  4. 分包策略:将稳定的RN框架层与业务层分离,大幅减小日常更新包体积
  5. 灰度发布:通过服务端配置条件过滤实现可控放量,降低生产事故影响面

扩展思考

  • Code Push vs App Center vs 自建:CodePush被微软收购并整合到App Center后逐渐停滞,自建方案(如Pushy、国内方案)成为主流
  • React Native 0.72+ 的 Hermes Bundle:Hermes引擎使用HBC格式的字节码bundle,无法像JS bundle那样直接替换,需要额外适配
  • Flutter的热更新:Flutter的官方态度是不支持热更新(Dart AOT编译约束),只能通过Google Play的分发功能实现类似效果,这是RN相对Flutter的一个重要差异化优势
  • ESBuild/Metro替代:随着Turbopack等新一代打包工具兴起,Metro的慢速打包问题有望得到改进
本文由作者按照 CC BY 4.0 进行授权