react-native 知识点记录

134 阅读25分钟

React-native 如何将JavaScript 代码转换为原生组件?

react Native 并非将 JavaScript 直接 “转换” 为原生组件,而是通过通信机制让 JS 逻辑控制原生组件的渲染和行为,流程如下:

JS 层定义 UI 结构:开发者用 React 语法(JSX)编写组件(如 <View>、<Text>),这些组件本质是 JS 对象,描述了 UI 结构和属性。
虚拟 DOM 映射:React 会将 JSX 转换为虚拟 DOM(JavaScript 对象),记录组件的类型、属性和层级关系。
通过 Bridge 传递指令:虚拟 DOM 的变更会被打包成序列化消息(JSON 格式),通过 Bridge 发送到原生层。
原生层渲染组件:原生层接收消息后,解析出组件类型(如 RCTView 对应 iOS 的 UIViewAndroid.View 对应 Android 的 View),并根据属性创建 / 更新原生组件,最终渲染到屏幕上。
事件回调:原生组件的事件(如点击、滚动)会反向通过 Bridge 传递给 JS 层,触发对应的回调函数。

桥接(Bridge)机制的作用

Bridge 是 React Native 中 JS 层与原生层之间的通信桥梁,主要作用包括:
跨语言通信:JS 运行在 JavaScript 引擎(如 JSC、Hermes)中,原生代码运行在平台的虚拟机(如 iOS 的 Objective-C 运行时、Android 的 ART)中,Bridge 负责两种不同运行环境的消息传递。
序列化与反序列化:JS 层的对象(如组件属性、事件参数)会被序列化为 JSON 字符串,原生层接收后反序列化为原生对象,反之亦然。
异步调度:JS 与原生的通信是异步非阻塞的,避免某一层的操作阻塞另一层(如 JS 计算不会卡住原生 UI 渲染)。
API 暴露:原生模块(如相机、蓝牙)通过 Bridge 向 JS 层暴露接口,JS 可以调用原生功能;同理,JS 也可以向原生层注册回调函数。

Bridge 机制的性能瓶颈

尽管 Bridge 实现了跨层通信,但由于其设计特性,存在以下性能瓶颈:

序列化开销:JS 与原生之间的每一次通信都需要对数据进行 JSON 序列化 / 反序列化,这对大量数据(如长列表、二进制数据)来说开销巨大,会导致延迟。
异步通信的局限性: 所有消息通过单一队列处理,高频率通信(如滚动事件、实时数据更新)会导致队列拥堵,出现 “掉帧”。 同步操作难以实现(如 JS 调用原生方法并立即获取结果),必须通过回调异步处理,增加了代码复杂度。
线程模型限制:JS 代码运行在单独的 JS 线程中,原生 UI 运行在主线程,Bridge 消息需要在不同线程间切换,切换成本可能导致延迟。 复杂计算若放在 JS 线程,会阻塞所有 JS 逻辑(包括 UI 更新),而通过 Bridge 分流到原生线程又会增加通信成本。
启动性能:初期 Bridge 初始化需要加载 JS 引擎、注册原生模块,这会增加应用的启动时间。

React Native 和原生开发(如 Swift/Kotlin)在性能、开发效率、适用场景上的优缺点?

标题性能开发效率
原生开发直接调用系统底层 API,UI 渲染、事件响应、动画执行均在主线程完成,无中间层开销。对于高频交互场景(如复杂动画、手势操作、3D 渲染、大型列表滚动),性能接近系统极限,几乎无卡顿。优势:直接使用平台官方工具链(Xcode/Android Studio),调试工具成熟,API 文档完善,对系统新特性(如 iOS 16 的 Widget、Android 13 的权限管理)支持即时且完整。劣势:1、双平台重复开发:iOS 和 Android 需分别编写代码,逻辑相同的功能(如登录页、网络请求)需实现两次,开发和维护成本翻倍。2、迭代周期长:原生代码编译耗时,尤其是大项目;发版需通过应用商店审核,热更新受限(iOS 禁止动态执行代码)。
react-native优势:通过 Fabric 新架构(同步渲染)和 Hermes 引擎(预编译 JS),性能已接近原生,普通页面(如表单、列表)的流畅度可满足大多数场景。 劣势:1、通信开销:JS 层与原生层通过桥接(或新架构的 JSI)通信,频繁数据交互(如实时传感器数据、高频动画)可能导致延迟。2、渲染限制:复杂自定义组件(如 OpenGL 绘制、粒子特效)需依赖原生封装,纯 JS 实现性能较差。3、启动时间:首次加载需解析 JS bundle,冷启动速度通常慢于原生应用。优势:1、一套代码多端运行:核心业务逻辑(UI、状态管理、网络请求)可在 iOS 和 Android 共享,开发效率提升 50% 以上,尤其适合中小型团队。2、热更新能力:通过 CodePush 等工具,JS 代码可绕过应用商店审核实时更新,紧急 Bug 修复几小时内生效。3、前端生态复用:可直接使用 npm 生态的库(如日期处理、表单验证),降低跨领域学习成本。劣势: 1、原生依赖不可避免:复杂功能(如蓝牙、人脸识别)需封装原生模块,仍需平台相关知识,无法完全脱离原生开发。2、调试复杂度高:JS 与原生交互的 Bug 难以定位,有时需同时调试两端代码。

React 类组件和函数组件的生命周期有何区别?useEffect 如何模拟 componentDidMount?

React 类组件和函数组件在生命周期管理上有本质区别,类组件通过显式的生命周期方法管理组件生命周期,而函数组件通过 useEffect 钩子函数实现类似功能,更加灵活且贴合函数式编程思想。

一、类组件与函数组件的生命周期区别

类组件通过继承 React.Component,使用预定义的生命周期方法(如 componentDidMountcomponentDidUpdate 等)管理组件从创建到销毁的过程,结构固定且阶段清晰。 核心生命周期阶段及对应方法:

挂载阶段:组件被创建并插入 DOM 时 constructor → render → componentDidMount

更新阶段:组件 props 或 state 变化时 shouldComponentUpdate → render → componentDidUpdate

卸载阶段:组件从 DOM 中移除时 componentWillUnmount

函数组件:useEffect 统一管理生命周期

函数组件本身没有生命周期方法,而是通过 useEffect 钩子模拟所有生命周期阶段。useEffect 将 “副作用”(如数据请求、事件监听)与组件的挂载、更新、卸载阶段关联,通过依赖项控制执行时机,更灵活。

useEffect 的核心逻辑:

接收两个参数:effect 函数(要执行的副作用)和 deps 依赖数组。

执行时机:
  1. 若 deps 为空数组 []:仅在组件挂载后执行一次(模拟 componentDidMount)。
  2. 若 deps 包含变量:组件挂载后执行一次,且当 deps 中变量变化时重新执行(模拟 componentDidMount + componentDidUpdate)。
  3. 若不指定 deps:组件每次渲染后都执行(不推荐,性能较差)。
  4. 清理机制:effect 函数可返回一个清理函数,在组件更新前或卸载时执行(模拟 componentWillUnmount)。

注意:若 useEffect 中使用了组件内的变量(如 props 或 state),但未加入依赖数组,可能导致闭包陷阱(读取到旧值)。此时需将变量加入 deps,但会失去 “仅挂载执行” 的特性,需根据场景调整。

如何为 iOS 和 Android 设置不同的导航栏样式?除了 Platform.OS,还有哪些方法?

一、使用 Platform.OS 直接判断(基础方法)

Platform.OS 是 React Native 内置 API,通过判断当前系统是 ios 还是 android,返回不同样式配置,适用于所有导航场景(栈导航、标签导航等)。

特点:
  • 优势:无额外依赖,灵活控制所有样式属性,适用于简单到复杂的差异化需求;
  • 不足:样式逻辑混在代码中,若差异点多,代码可读性会下降。

二、使用 Platform.select 集中管理

Platform.select 允许将不同平台的配置集中定义在一个对象中,自动根据当前平台返回对应配置,避免多次写 Platform.OS 判断,代码更整洁。

import { Platform } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

// 集中定义平台差异化配置
const navConfig = Platform.select({
  ios: {
    headerStyle: {
      backgroundColor: '#FFFFFF',
      elevation: 0,
    },
    headerTintColor: '#000000',
    headerTitleAlign: 'center', // iOS 标题默认居中
  },
  android: {
    headerStyle: {
      backgroundColor: '#2196F3',
      elevation: 4,
    },
    headerTintColor: '#FFFFFF',
    headerTitleAlign: 'left', // Android 标题默认居左
  },
});

const AppStack = () => {
  return (
    <Stack.Navigator screenOptions={navConfig}>
      <Stack.Screen name="Home" component={HomeScreen} />
    </Stack.Navigator>
  );
};

三、分平台文件(推荐用于复杂场景)

通过 文件名后缀 区分平台专用文件(如 NavbarStyles.ios.js 和 NavbarStyles.android.js),React Native 会自动根据运行平台加载对应文件,彻底隔离平台差异。

优势

  • 完全隔离平台逻辑,适合差异极大的场景(如 iOS 和 Android 导航栏结构完全不同);

  • 多人协作时,不同平台开发者可独立维护各自文件,减少冲突;

  • 扩展性强,若后续需添加其他平台(如 Web),只需新增对应后缀文件。

React Navigation 的堆栈导航(Stack Navigator)和底部导航(Tab Navigator)如何结合使用?

在 React Navigation 中,堆栈导航(Stack Navigator)和底部导航(Tab Navigator)的结合是非常常见的场景,通常通过 导航嵌套 实现。

在 React Navigation 中,Stack 嵌套 Tab 和 Tab 嵌套 Stack 都是支持的,具体采用哪种嵌套方式取决于业务场景。

一、两种嵌套方式的区别与适用场景

1. Tab 嵌套 Stack(最常用)

结构:底部 Tab 作为根导航,每个 Tab 项是一个独立的 Stack 导航器。

适用场景: 大多数常规 App(如电商、社交),每个标签页是独立模块(如 “首页”“我的”),模块内部有层级跳转需求。 切换 Tab 时,保留每个 Tab 内部的页面栈状态(例如切换到 “我的” 再切回 “首页”,首页的跳转状态不会丢失)。

2. Stack 嵌套 Tab(特殊场景)

结构:Stack 作为根导航,某个 Stack 页面中嵌套 Tab 导航器。

适用场景: 需先完成某个操作(如登录)才能进入 Tab 页面,Tab 页面是应用的主内容区。 局部页面需要 Tab 切换,而不是全局底部 Tab(例如在 “个人中心” 内部用 Tab 切换 “我的订单”“我的收藏”)。

二、关键注意事项

导航作用域: 嵌套在内部的导航器(如 Tab 里的 Stack)只能操作自身作用域内的页面(例如 Tab1 的 Stack 无法直接跳转到 Tab2 的 Stack 页面,需通过根导航或状态管理实现跨 Tab 通信)。
外层导航器(如根 Stack)可以操作所有内层导航器的页面(例如从根 Stack 跳转到 Tab 内部的页面)。

导航栏显示: 嵌套时需通过 headerShown: false 控制导航栏显示,避免多层导航栏叠加(例如 Tab 嵌套 Stack 时,Tab 页面通常隐藏自身导航栏,使用 Stack 的导航栏)。

状态保留: Tab 嵌套 Stack 时,切换 Tab 会保留每个 Stack 的状态(页面栈、输入内容等)。 Stack 嵌套 Tab 时,Tab 内部的状态会随 Stack 页面卸载而丢失(如需保留需用 persist 等方案)。

在什么场景下需要 Redux?如何避免过度使用全局状态?

Redux 是一种集中式状态管理方案,主要解决跨组件、跨页面共享状态的问题,但并非所有场景都需要使用。合理使用 Redux 的核心是区分局部状态和全局状态,避免将所有状态都放入全局管理。

一、需要使用 Redux 的典型场景

  1. 多组件共享同一状态 当多个不相关的组件(如头部导航、侧边栏、内容区)需要依赖同一份数据时,使用 Redux 可避免通过 props 层层传递(“props 钻取” 问题)。

    例:用户登录状态(用户名、权限)需要在导航栏、个人中心、设置页等多处展示或使用。

  2. 状态需要在多个页面间共享

    跨页面(路由)的状态共享,尤其是通过导航跳转后仍需保留的状态。

    例:电商 App 的购物车数据,在商品列表页添加商品后,切换到购物车页面能立即看到更新。

  3. 状态修改逻辑复杂或需要追溯

    当状态变更涉及多个步骤、多组数据联动,或需要记录状态变更历史(如撤销 / 重做)时,Redux 的单向数据流和 action 机制能让状态变化可预测、可调试。

    例:表单多步骤提交(如注册流程)、复杂筛选条件的保存与恢复。

  4. 需要中间件处理异步逻辑

    当状态变更依赖异步操作(如 API 请求),且需要统一管理加载状态、错误处理时,Redux 结合中间件(如 Redux Thunk、Redux Saga)能规范化异步流程。

    例:分页加载列表数据(管理 loading/data/error 状态)、实时数据更新(WebSocket 连接)。

  5. 大型应用的状态治理

    团队协作的大型项目中,统一的状态管理规范(如 action 命名、reducer 拆分)可提高代码一致性,降低维护成本。

二、如何避免过度使用全局状态?

过度使用 Redux 会导致代码冗余、性能下降(不必要的重渲染)和逻辑复杂化。避免过度使用的核心原则是:“能局部管理的状态,就不放入全局”。

  1. 优先使用组件内部状态(useState/useReducer)

    仅在组件内部使用的状态,无需放入 Redux。

  2. 使用 React 上下文(Context)管理局部共享状态

    对于仅在某一组件树内共享的状态(而非全应用),使用 Context 更轻量,避免 Redux 的样板代码。

  3. 明确全局状态的 “最小必要集”

    只将 “必须跨组件 / 跨页面共享” 的状态放入 Redux,避免将 “可能用到” 的状态提前全局化。

  4. 拆分状态粒度,避免 “大而全” 的全局状态

    按业务域拆分 Redux 状态(如 user、cart、settings),每个域只包含相关状态,避免一个庞大的 state 对象。

  5. 使用中间件按需加载状态

    对于大型应用,可通过 Redux 中间件(如 Redux Toolkit 的 createAsyncThunk)按需加载状态,避免初始化时加载所有全局数据导致性能问题。

FlatList 和 ScrollView 的区别是什么?如何优化长列表的滚动卡顿?

渲染机制:ScrollView一次性渲染所有子元素,无论是否可见;FlatList只渲染当前可见区域及少量缓冲区元素

性能表现:ScrollView数据量大时(>20 条)会导致内存飙升、渲染卡顿;FlatList数据量大时(上百 / 上千条)仍能保持流畅

长列表滚动卡顿的优化手段(基于 FlatList)

FlatList 本身已针对长列表做了优化,但在数据量极大或列表项复杂时仍可能卡顿,需从以下方面优化:

  1. 必须设置 keyExtractor 或 key 属性。 为每个列表项提供唯一标识,帮助 React Native 区分不同项,避免不必要的重渲染
  2. 使用 getItemLayout 减少测量开销
    FlatList 会默认测量每个列表项的高度 / 宽度,这是性能瓶颈之一。若列表项高度固定,通过 getItemLayout 预先告知尺寸,可跳过测量步骤
  3. 优化列表项组件(减少重渲染)
    使用 memo 缓存列表项:避免列表项因父组件重渲染而不必要刷新;
    减少列表项复杂度:避免在列表项中嵌套复杂组件(如大量图片、动画),可采用懒加载或简化布局。
  4. 控制渲染区域(windowSize
    windowSize 定义可见区域外额外渲染的列表项数量(默认 5),值越小性能越好,但快速滚动时可能出现空白。
  5. 启用虚拟列表优化(removeClippedSubviews
    在 Android 上启用 removeClippedSubviews,自动移除屏幕外的列表项视图,减少内存占用。
  6. 图片优化
    列表项中的图片使用 resizeMode 控制缩放,避免图片过大。
    使用 react-native-fast-image 等库实现图片缓存和懒加载。
  7. 数据分片加载(避免一次性加载全部数据)
    通过 onEndReached 实现分页加载,每次只加载部分数据(如 20 条)
  8. 避免在 renderItem 中定义函数
    renderItem 中定义匿名函数会导致每次渲染创建新函数,触发列表项重渲染

如何避免组件不必要的重渲染?React.memo 和 useMemo 的区别是什么?

在 React 中,组件的 “不必要重渲染” 是指组件在 props/state 未发生实质性变化 时仍触发渲染,会导致性能浪费(尤其在列表、复杂组件中)。避免这一问题需要从 “控制渲染触发条件” 入手,而 React.memouseMemo 是核心优化工具,但适用场景和原理截然不同。

一、如何避免组件不必要的重渲染?

  1. 确保子组件接收的 props 稳定
    避免传递 “动态创建的 props”:如匿名函数、临时对象、字面量数组,这类 props 每次渲染都会生成新引用,导致子组件误判为 “props 变化”。
  2. React.memo 包装纯函数组件
    对 “仅依赖 props 渲染” 的纯函数组件,用 React.memo 包装,使其仅在 props 发生浅比较变化 时才重渲染(默认浅比较,可自定义比较逻辑)。
  3. useMemo 缓存组件内的计算结果
    若组件内有 “耗时计算逻辑”(如列表过滤、数据转换),用 useMemo 缓存计算结果,避免每次渲染重复执行计算。
  4. useReducer 替代复杂 state
    当组件有多个关联 state 时,useReducer 的 dispatch 函数引用永久稳定(不会因渲染变化),可避免子组件因 “传递 state 更新函数” 导致的重渲染。

二、React.memo 和 useMemo 的区别

两者虽都用于 “减少不必要计算 / 渲染”,但 适用对象、作用范围、原理完全不同,核心区别如下:

标题React.memouseMemo
作用对象针对 “函数组件”(整体)针对 “组件内的计算结果”(值 / 对象 / 数组)
核心作用控制 “组件是否重渲染”控制 “计算结果是否重复执行”
使用方式作为 “高阶组件” 包装组件(外部包装)作为 “钩子函数” 在组件内部调用(内部缓存)
比较逻辑默认对 props 做 “浅比较”(可自定义比较函数)对 “依赖数组” 做浅比较,依赖不变则返回缓存值
返回值返回一个 “记忆化的组件”(可直接渲染)返回一个 “记忆化的值”(需手动传递给子组件)
React.memo:缓存 “组件”,控制子组件重渲染

适用场景:子组件是 “纯函数组件”(渲染仅依赖 props,无副作用),且父组件频繁重渲染但传递的 props 稳定。
自定义比较逻辑:默认浅比较无法满足需求时(如深层对象),可传入第二个参数(比较函数)。

useMemo:缓存 “计算结果”,避免重复计算

适用场景:组件内有 “耗时计算”(如大数据过滤、复杂数据转换),且计算结果仅依赖特定依赖项。

为什么需要 useCallback?

函数在每次组件渲染时都会重新创建(引用变化),即使逻辑完全相同。若子组件用 React.memo 包装,会因函数引用变化而重渲染,useCallback 可解决这一问题。

列举常见的 React Native 内存泄漏场景,如何预防?

在 React Native 中,内存泄漏通常源于未正确清理的资源引用(如定时器、事件监听、网络请求等),导致组件卸载后相关资源仍被占用,无法被垃圾回收。

一、常见内存泄漏场景及预防

未清理的定时器(setTimeout/setInterval)

组件中设置定时器后,未在卸载时清除,导致定时器回调持续执行,且回调中若引用组件状态 / 方法,会间接持有组件实例,阻止其被回收。

未移除的事件监听器(全局 / 原生事件)

监听全局事件(如 window.scroll、Dimensions.addEventListener)或原生模块事件(如 BLE 设备通知、传感器数据)后,未在组件卸载时移除,导致事件回调持续触发,且持有组件引用。

未取消的异步操作(网络请求、Promise)

组件发起异步请求(如 fetch、Axios)后,在请求完成前卸载,此时回调函数若试图更新组件状态(如 setState),会导致警告,且回调持有组件引用,阻碍回收。

未释放的原生资源(如定时器、动画、BLE 连接)

使用原生模块(如 Animated 动画、BLE 连接、摄像头)时,若未正确停止或断开连接,原生层资源会持续占用,导致内存泄漏(JS 层组件已卸载,但原生层资源未释放)。

闭包中持有过期的组件引用

回调函数(如定时器、事件监听)通过闭包引用了组件的状态或方法,组件卸载后,这些闭包仍持有旧的组件引用,导致无法回收。

如何通过 React Native 调用 Android 的原生 Toast 模块?

在 React Native 中调用 Android 原生 Toast 模块,需要通过 原生模块封装 实现,核心是创建一个 Android 原生模块并暴露给 JS 层调用。

原生层:创建模块类 → 注册模块 → 暴露方法;

JS 层:获取原生模块 → 封装调用接口 → 在组件中使用。

如何在现有原生应用中集成 React Native 页面?需要哪些配置?

在现有原生应用中集成 React Native页面,核心是通过 ReactRootView 嵌入 RN 组件,并通过 Metro Bundler 加载 JS 代码。

ReactRootView(Android)/RCTRootView(iOS):

原生平台用于承载 RN 组件的容器视图,负责与 JS 引擎通信、渲染 RN 界面,并同步生命周期事件(如 onResume/onPause)。

Metro Bundler:

RN 官方的 JS 打包工具,负责将 JS 代码、图片等资源打包成 bundle 文件。调试时以服务形式运行(默认端口 8081),原生应用通过网络请求加载 JS 代码;release 时需提前打包为离线 bundle(index.android.bundle/main.jsbundle)嵌入应用。

模块注册:

RN 组件需通过 AppRegistry.registerComponent 注册,原生端通过注册名称(如 RNPage)加载对应的组件,确保两端名称一致。

现有原生应用集成 RN 页面的核心步骤是:

  1. 配置 RN 开发环境和依赖
  2. 通过 ReactRootView/RCTRootView 在原生页面中嵌入 RN 容器
  3. 启动 Metro Bundler 调试,或打包离线 bundle 用于发布

如何实现相机拍照并上传功能?需要哪些权限和第三方库?

一、核心依赖库

  1. react-native-camera相机核心库(支持拍照、录像)
  2. react-native-permissions权限管理库
  3. react-native-fs文件处理库(用于获取图片路径、转换格式)
  4. axios 网络请求库(可选,也可使用 fetch/axios)

二、权限申请

在Android和iOS各自平台添加相册和拍照权限描述

三、关键功能解析

权限管理:

使用 react-native-permissions 统一处理 Android/iOS 权限申请,确保用户授予相机权限后才显示相机界面。

相机调用:

通过 RNCamera 组件实现相机预览,type 属性控制前后置摄像头。
takePictureAsync 方法拍摄照片,返回照片的本地 URI(路径)。

照片上传:

使用 FormData 构造 multipart/form-data 格式数据(符合大多数后端接口要求)。
注意处理 iOS 和 Android 的路径差异(iOS 路径需移除 file:// 前缀)。
通过 onUploadProgress 监听上传进度(可选功能)。

用户体验:

拍摄后显示预览界面,提供 “重拍” 和 “上传” 选项。
错误处理(权限拒绝、拍摄失败、上传失败)通过 Alert 提示用户。

如何调试 React Native 中的网络请求?如果遇到原生层崩溃,如何定位问题?

调试 React Native 网络请求调试方法

网络请求调试需兼顾 JS 层 和 原生层,常用工具和技巧如下:
Chrome DevTools(基础网络调试)React Native 支持通过 Chrome 调试 JS 代码,同时可查看网络请求详情:
开启方式: 摇一摇设备 → 选择「Debug JS Remotely」→ 自动打开 Chrome 调试页面(chrome://inspect)。
网络面板使用: 在 Chrome 调试页面的「Network」面板中,可查看所有 fetch 或 axios 发起的网络请求,包括: 请求 URL、方法、 headers、参数;响应状态码、响应体、耗时;可筛选请求类型(XHR/fetch),复制请求信息用于复现问题。

原生层崩溃定位方法

生层崩溃(Crash)通常表现为 App 突然闪退,无 JS 层错误提示,需通过原生工具分析:

  1. Android 原生崩溃定位 工具:Android Studio + Logcat
  2. iOS 原生崩溃定位 工具:Xcode + 设备日志 + Instruments

CodePush 的原理是什么?如何保证热更新失败时的回滚安全?

CodePush 是微软提供的 React Native 热更新解决方案,允许开发者在不经过应用商店审核的情况下,向用户推送 JS 代码、图片等资源更新。其核心价值在于快速修复 bug 或发布小功能,同时需通过严谨的机制保证更新安全。

CodePush 的工作原理

CodePush 的核心是动态替换应用中的 JS bundle 和静态资源,整体流程分为 “更新发布” 和 “客户端更新” 两部分:

发布更新流程(开发者视角)

开发者通过 CodePush CLI 将打包后的 JS bundle(index.bundle)和静态资源(图片、字体等)上传到 CodePush 服务器。
上传时需指定更新的目标版本(如 1.0.0)、更新描述、强制更新标识等信息。
CodePush 服务器存储更新包,并为不同版本的应用维护更新记录。

客户端更新流程(用户视角)

检查更新:应用启动或运行时,客户端 SDK 向 CodePush 服务器发起请求,携带当前应用版本号、唯一设备 ID 等信息。
对比版本:服务器根据客户端信息判断是否有适用于该设备 / 版本的更新,返回更新包下载地址。
下载更新:客户端后台下载更新包(支持断点续传),并验证包的完整性(通过哈希校验)。
非强制更新:通常在应用下次启动时,用新的 JS bundle 替换旧 bundle,完成更新。
强制更新:立即提示用户重启应用以应用更新。
状态反馈:客户端将更新结果(成功 / 失败)上报给 CodePush 服务器,便于开发者监控更新效果。

核心技术点

JS bundle 替换:React Native 应用的逻辑主要存于 JS bundle 中,CodePush 通过替换该文件实现功能更新,无需修改原生代码(.apk/.ipa)。
增量更新:CodePush 会计算新旧 bundle 的差异(delta),仅传输差异部分,减少下载体积。
版本匹配:通过应用版本号(versionName/CFBundleShortVersionString)控制更新范围,避免向不兼容版本推送更新。

保证热更新失败时的回滚安全

热更新失败(如 JS 语法错误、资源缺失、与原生代码不兼容)可能导致应用崩溃,CodePush 通过多层机制确保可安全回滚:

预验证机制(防止错误更新被安装)

本地校验:客户端下载更新包后,会先校验包的完整性(哈希值匹配)和兼容性(如 JS 版本与原生模块版本是否匹配),校验失败则直接丢弃更新。
开发者预览:发布正式更新前,可通过 --rollout 参数灰度发布(如先推送给 10% 用户),监控崩溃率,发现问题可立即暂停。

自动回滚机制(更新失败后恢复)

启动验证:应用首次加载新更新时,CodePush 会监听应用是否正常启动(通过 CodePush.notifyAppReady() 确认)。
若应用成功启动并调用 notifyAppReady(),则标记更新为 “有效”。
若启动失败(如崩溃、超时未调用 notifyAppReady()),CodePush 会自动回滚到上一个稳定版本的 bundle。

手动回滚机制(开发者主动干预)

CLI 回滚:若发现更新有问题,开发者可通过 CLI 命令回滚特定版本的更新。
客户端强制回滚:通过代码触发回滚(适用于检测到特定错误时)

版本管理策略(减少回滚风险)

环境隔离:使用 CodePush 的部署环境(如 Staging 测试环境、Production 生产环境),先在测试环境验证更新,再推到生产环境。
版本锁定:确保热更新仅适用于特定原生版本(如 appVersion: "1.0.x"),避免向原生代码已变更的版本推送不兼容的 JS 更新。
完整备份:CodePush 会自动备份旧版本的 bundle 和资源,回滚时直接恢复备份,无需重新下载