React Native 底层原理
1. 为什么选择 React Native?
React Native 让我们只写一份代码,就能同时在 iOS、Android、Web 三个平台上运行。 它不是"网页套壳",而是会把我们写的代码翻译成各平台真正的原生组件来显示。
| 方案 | 开发成本 | 性能 | 维护成本 |
|---|---|---|---|
| 各端独立开发 | 高(3 套代码) | 最优 | 高 |
| Flutter | 中 | 优 | 中 |
| React Native | 中 | 良~优 | 低 |
| WebView 套壳 | 低 | 差 | 低 |
我们选择 React Native 的核心原因:
- 一套业务逻辑代码覆盖 iOS、Android、Web 三端
- 可以直接调用原生播放器 SDK,视频渲染性能不打折
- 团队已有 React/TypeScript 技术栈积累
2. React Native SDK 对比原生 SDK 的优势
如果客户目前在 iOS、Android、Web 三端分别集成各自的原生播放器 SDK,切换到我们的 React Native SDK 能带来以下好处:
对客户研发团队
| 对比项 | 各端独立集成原生 SDK | 使用 RN 播放器 SDK |
|---|---|---|
| 需要的开发人员 | iOS + Android + Web 各至少 1 人 | 1-2 人即可覆盖三端 |
| 接口学习成本 | 学 3 套不同的 API | 学 1 套统一的 TypeScript API |
| 功能对齐 | 三端分别开发,容易出现功能差异 | 一份代码,三端功能天然一致 |
| Bug 修复 | 同一个 Bug 可能要修 3 次 | 修一次,三端同时生效 |
| 新功能上线 | 三端排期协调,上线时间不一致 | 一次开发,同步上线 |
对客户业务
| 对比项 | 各端独立集成原生 SDK | 使用 RN 播放器 SDK |
|---|---|---|
| 集成方式 | 每端不同的集成流程和配置 | npm install 一行命令,三端通用 |
| 版本管理 | 三端 SDK 版本可能不一致 | 统一版本号,统一升级 |
| 交付周期 | 新需求要等三端都开发完 | 开发周期缩短至少一半 |
| 用户体验一致性 | 三端交互可能有差异 | 统一的交互逻辑和状态管理 |
性能方面不打折
客户最常问的问题是"跨平台方案性能行不行",答案是:
- 视频解码和渲染走的是各平台原生播放器(iOS 用 VNPlayer,Android 用 VnspPlayerKit),跟直接用原生 SDK 完全一样
- RN 层只负责控制逻辑(播放/暂停/切换等),这些操作本身非常轻量,不影响播放性能
- 简单说:画面是原生画的,RN 只是按了一下遥控器
一句话总结
客户不需要为三个平台养三支团队、维护三套代码。用我们的 SDK,一个前端就能搞定三端播放器集成,功能一致、体验一致、版本一致,而且视频播放性能跟原生没有区别。
3. React Native 的工作方式
React Native 就像一个"翻译官"系统:
- 你(开发者)说的是"JavaScript 语"
- iOS 手机只听得懂"Swift/OC 语"
- Android 手机只听得懂"Kotlin/Java 语"
- 浏览器只听得懂"HTML/CSS/JS 语"
React Native 在中间做翻译:
- 你写
<View style={{backgroundColor: 'red'}}> - 在 iOS 上,翻译成
UIView并设置红色背景 - 在 Android 上,翻译成
android.view.View并设置红色背景 - 在 Web 上,翻译成
<div style="background-color: red">
屏幕上显示的按钮、文字、输入框,都是各平台真正的原生组件,不是模拟出来的,所以外观和交互体验跟原生 App 一致。
4. 整体架构
graph TD
subgraph JS_Layer["JavaScript 层"]
JS["React 组件 + 业务逻辑<br/>(TypeScript 编写)"]
Hermes["Hermes 引擎<br/>(执行 JS 代码)"]
JS --> Hermes
end
subgraph Core["React Native 核心层"]
JSI["JSI<br/>(JS 与原生的直接通信通道)"]
Fabric["Fabric 渲染器<br/>(负责把组件显示到屏幕)"]
Turbo["Turbo Modules<br/>(按需加载原生功能)"]
end
subgraph Native["原生层"]
iOS["iOS 原生<br/>UIKit / VNPlayer"]
Android["Android 原生<br/>Android View / VnspKit"]
Web["Web DOM<br/>浏览器 / vnsp.js"]
end
Hermes --> JSI
JSI --> Fabric
JSI --> Turbo
Fabric --> iOS
Fabric --> Android
Fabric --> Web
Turbo --> iOS
Turbo --> Android
Turbo --> Web
5. 核心机制简介
(1)Hermes — JavaScript 引擎
用 C++ 编写,专门为 React Native 打造的 JS 引擎,在打包阶段就把代码预编译成字节码,启动更快、内存更省。
(2)JSI — JavaScript Interface
一层 C++ 接口,JS 和原生之间的直接通信通道,取代了早期需要序列化排队的 Bridge,让两边可以"面对面对话"。
(3)Fabric — 渲染器
用 C++ 实现的新一代 UI 渲染模块,支持同步渲染和优先级调度,滑动更流畅、不容易白屏。
(4)CodeGen — 代码生成工具
用 JavaScript 编写的编译期工具,根据 JS/TS 的接口定义自动生成原生端代码(OC/Java/C++),编译时就能发现两端接口不一致的问题。
(5)Turbo Modules — 原生功能管理
通过 C++ 和各平台原生语言(OC/Kotlin)实现的模块加载机制,用到哪个才加载哪个,通过 JSI 直接通信,启动更快、调用更高效。
6. 深入底层:Hermes 引擎
6.1 Hermes 的编译流程
传统 JS 引擎(如 V8、JavaScriptCore)在 App 运行时才把 JS 源码编译成机器能执行的指令,这个过程叫 JIT(Just-In-Time,即时编译)。Hermes 的做法不同:
graph LR
subgraph 传统引擎["传统 JS 引擎(运行时编译)"]
A1["JS 源码"] --> A2["App 启动时解析"] --> A3["编译为机器码"] --> A4["执行"]
end
subgraph Hermes["Hermes(提前编译)"]
B1["JS 源码"] --> B2["打包时编译为字节码(.hbc)"] --> B3["App 启动直接执行字节码"]
end
Hermes 在打包阶段就把 JS 编译成了字节码(.hbc 文件),App 启动时跳过了解析和编译步骤,直接执行。这就是为什么 Hermes 启动快的根本原因。
6.2 Hermes 的内存管理
Hermes 使用分代垃圾回收(Generational GC):
- 新创建的对象放在"年轻代",回收频率高但速度快
- 存活时间长的对象晋升到"老年代",回收频率低
- 这种策略避免了一次性扫描全部内存导致的卡顿
对比 V8 引擎,Hermes 在移动端的内存占用通常低 30%-50%,因为它不需要维护 JIT 编译器的缓存。
6.3 Hermes 与标准 JS 引擎的差异
Hermes 为了保持轻量,对部分 JS 特性的支持与 V8 等引擎有所不同:
eval()和Function()构造器:有限支持,复杂的场景可能会失败或行为不一致with语句:不支持,编译阶段就会报错
这些差异在实际开发中几乎没有影响,因为现代 JS 开发本身就不推荐使用这些特性
7. 深入底层:JSI 的实现原理
7.1 早期版本(0.68 之前)Bridge 的问题
React Native 在 0.68 版本之前,JS 和 Native 之间通过一个异步的 Bridge 通信:
sequenceDiagram
participant JS as JS 线程
participant Bridge as Bridge(异步队列)
participant Native as Native 线程
JS->>Bridge: JSON.stringify({module, method, args})
Note over Bridge: 消息入队,等待处理
Bridge->>Native: JSON.parse() → 找到模块 → 调用方法
Native->>Bridge: JSON.stringify(result)
Bridge->>JS: JSON.parse(result)
这个过程的瓶颈:
- 每次通信都要做 JSON 序列化/反序列化(把对象转成字符串再转回来),数据量大时很慢
- Bridge 是异步队列,消息要排队,高频调用时会堆积
- JS 和 Native 运行在不同线程,无法同步获取返回值
7.2 JSI 如何解决这些问题
JSI 是一层 C++ 接口,它让 JS 引擎可以直接持有 Native 对象的引用:
sequenceDiagram
participant JS as JS 线程
participant JSI as JSI (C++ 层)
participant Native as Native 代码
JS->>JSI: 调用 global.__turboModuleProxy.get("PlayerModule")
JSI->>Native: 返回 C++ HostObject 引用(不序列化)
JS->>JSI: playerModule.startLive(url)
JSI->>Native: 直接调用 C++ 函数(同步)
Native-->>JS: 直接返回结果
关键改进:
- 不再需要 JSON 序列化,JS 直接持有 Native 对象的 C++ 引用
- 支持同步调用,不需要等异步队列
- JS 可以直接调用 C++ 函数,C++ 再调用 OC/Kotlin/Java
7.3 JSI HostObject 机制
JSI 的核心概念是 HostObject——一个由 C++ 实现、但可以在 JS 中像普通对象一样使用的东西。
当 JS 访问 HostObject 的属性或调用方法时,实际上是在调用 C++ 的 get 和 set 方法。这就是"直接通信"的本质:JS 操作的对象背后直接连着 Native 代码,中间没有序列化、没有队列、没有线程切换。
8. 深入底层:Fabric 渲染器
8.1 渲染流程
当你写了一个 React 组件,它从代码变成屏幕上的像素,经历以下步骤:
graph TD
A["React 组件状态变化"] --> B["React 生成新的组件树(Virtual DOM)"]
B --> C["Diff 算法:对比新旧组件树,找出变化"]
C --> D["Shadow Tree:用 Yoga 引擎计算布局(位置、大小)"]
D --> E["生成原生操作指令(创建/更新/删除 View)"]
E --> F["提交到 Native 主线程执行渲染"]
8.2 Yoga 布局引擎
React Native 使用 Yoga(C++ 实现的 Flexbox 布局引擎)来计算每个组件的位置和大小。
为什么不直接用各平台的布局系统?因为 iOS 用 Auto Layout,Android 用 ConstraintLayout,Web 用 CSS,三套规则不一样。Yoga 提供了一套统一的 Flexbox 布局规则,在 C++ 层计算好之后,再把结果告诉各平台去渲染。
这也是为什么 React Native 的样式写法跟 CSS 很像(都是 Flexbox),但又不完全一样——因为它用的是 Yoga 的实现,不是浏览器的 CSS 引擎。
一个具体的例子
假设我们在 RN 层写了一个简单的播放器容器布局:
<View style={{ flex: 1, backgroundColor: '#000' }}>
<View style={{ width: 320, height: 180, alignSelf: 'center', marginTop: 20 }}>
{/* 播放器画面 */}
</View>
<Text style={{ color: '#fff', textAlign: 'center', marginTop: 10 }}>
正在播放
</Text>
</View>
Yoga 引擎会先把这段样式计算成具体的像素值(比如在 iPhone 15 上:外层 View 宽 393px 高 852px,播放器 View 位于 x=36.5 y=20 宽 320 高 180...),然后交给各平台去渲染:
<View style={{flex:1, backgroundColor:'#000'}}>(黑色全屏容器)
- iOS → 创建
UIView,设置backgroundColor = .black - Android → 创建
android.view.View,调用setBackgroundColor(Color.BLACK) - Web → 生成
<div style="display:flex; flex:1; background-color:#000">
<View style={{width:320, height:180}}>(播放器画面区域)
- iOS → 创建
UIView,设置 frame(0, 0, 320, 180) - Android → 创建
View,设置 LayoutParams(320dp, 180dp) - Web → 生成
<div style="width:320px; height:180px">
<Text style={{color:'#fff'}}>正在播放</Text>(白色文字)
- iOS → 创建
UILabel,设置textColor = .white,text = "正在播放" - Android → 创建
TextView,调用setTextColor(Color.WHITE),setText("正在播放") - Web → 生成
<span style="color:#fff">正在播放</span>
开发者只写一次布局代码,Yoga 算好位置,各平台用自己的原生组件渲染出来。最终用户看到的效果一致,但底层用的是完全不同的原生控件。
8.3 Fabric 的优势
- 可以在任意线程准备渲染数据,不再受限于固定线程
- 支持同步渲染,用户滑动列表时不会出现白屏
- 支持渲染优先级,用户正在交互的部分优先刷新,后台更新排后面
- 通过 JSI 与 JS 层通信,比早期版本的异步 Bridge 快得多
实际体感:列表滑动更流畅,页面切换时不容易出现白屏闪烁。
9. CodeGen 是什么
CodeGen 是 React Native 新架构中的一个工具,它能根据 JS/TS 的 Spec 文件自动生成原生端的接口代码(OC 协议、Java/Kotlin 抽象类等)。
CodeGen 的核心价值是编译期类型检查——如果 JS 端调用的方法签名和原生端实现不一致,编译时就会报错,而不是等到运行时才崩溃。
我们项目 Android 端已经接入了 CodeGen,通过在 package.json 中配置 codegenConfig,构建时自动生成 NativeVnspPlayerModuleSpec 抽象类,ReactVnspPlayerModule 继承该类并实现所有方法。
9.1 CodeGen 做了什么
graph LR
A["JS/TS Spec 文件<br/>(定义模块接口)"] --> B["CodeGen 工具<br/>(编译时运行)"]
B --> C1["生成 OC 协议头文件"]
B --> C2["生成 Java/Kotlin 接口"]
B --> C3["生成 C++ TurboModule 胶水代码"]
举个例子,如果你写了一个 Spec:
export interface Spec extends TurboModule {
startLive(viewTag: number, url: string): void;
pause(): void;
}
CodeGen 会自动生成对应的 OC 协议:
@protocol NativePlayerModuleSpec <RCTBridgeModule, RCTTurboModule>
- (void)startLive:(double)viewTag url:(NSString *)url;
- (void)pause;
@end
原生模块只需要实现这个协议,就能保证 JS 和 Native 的接口定义完全一致。
10. 深入底层:Native Modules vs Turbo Modules
10.1 我们项目当前的情况
| 配置项 | Android | iOS |
|---|---|---|
| 新架构(Fabric) | ✅ 已开启 | ❌ 未开启 |
| Hermes 引擎 | ✅ 已开启 | ❌ 未开启(用的 JavaScriptCore) |
| 原生模块方式 | ✅ Turbo Modules | 旧版 Native Modules |
Android 端已经完成了 Turbo Modules 迁移,并接入了 CodeGen:
ReactVnspPlayerModule继承 CodeGen 生成的NativeVnspPlayerModuleSpec抽象类,编译期保证接口一致ReactVnspPlayerPackage从ReactPackage改为继承TurboReactPackage,支持懒加载- JS 端通过
TurboModuleRegistry.getEnforcing("VnspPlayerModule")获取模块实例,Spec 文件同时作为 CodeGen 的输入源
iOS 端仍然使用旧版 Native Modules:
- 继承
RCTEventEmitter,用RCT_EXPORT_METHOD宏暴露方法 - JS 端通过
NativeModules.NativeNavigatorModule获取模块实例
10.2 旧版 Native Modules 的工作方式(iOS 端当前状态)
sequenceDiagram
participant JS as JS 层
participant Bridge as Bridge(异步队列)
participant Native as 原生模块
JS->>Bridge: NativeModules.NativeNavigatorModule.playLivingStart(params)
Note over Bridge: 参数序列化 → 入队
Bridge->>Native: 反序列化 → 找到模块 → 调用方法
Native->>Bridge: sendEventWithName("NativeCallbackEvent", body)
Note over Bridge: 事件序列化 → 入队
Bridge->>JS: emitter 触发回调
特点:
- 所有通信都经过 Bridge 异步队列,有序列化开销
- App 启动时所有注册的模块都会初始化
- 无法同步获取返回值
10.3 Turbo Modules 的工作方式(Android 端当前状态)
sequenceDiagram
participant JS as JS 层
participant JSI as JSI(C++ 直连)
participant Native as 原生模块
JS->>JSI: TurboModuleRegistry.getEnforcing("VnspPlayerModule")
Note over JSI: 首次访问才创建实例(懒加载)
JSI->>Native: playerModule.startLive(viewTag, url, deviceId)
Note over JSI: 直接调用,无序列化
Native-->>JS: 可同步返回结果
优势:
- 通过 JSI 直接调用,不经过 Bridge,没有序列化开销
- 懒加载:用到哪个模块才初始化哪个,App 启动更快
- 支持同步返回值
- JS Spec 文件提供了完整的类型定义,调用时有类型检查
10.4 Android 端迁移做了什么
实际改动了 4 个文件,业务逻辑完全没动:
| 文件 | 改动内容 |
|---|---|
新增 NativeVnspPlayerModule.ts | JS Spec 文件,用 TypeScript 定义模块所有方法的参数和返回值类型(同时作为 CodeGen 的输入源) |
改写 AndroidPlayerAdapter.ts | 从 NativeModules.VnspPlayerModule 改为导入 Spec 文件获取模块实例 |
改写 ReactVnspPlayerModule.kt | 继承 CodeGen 生成的 NativeVnspPlayerModuleSpec 抽象类,编译期保证接口一致 |
改写 ReactVnspPlayerPackage.kt | 从 ReactPackage 改为 TurboReactPackage,实现懒加载注册 |
PlayerService、WebPlayerAdapter、ReactVideoView、ReactVnspViewManager、MainApplication 都不需要改。
10.5 iOS 端后续迁移计划
iOS 端要迁移到 Turbo Modules,需要:
- 在 Podfile 中开启新架构(
hermes_enabled、fabric_enabled、new_arch_enabled改为true) - 新增 JS Spec 文件,定义 NativeNavigatorModule 的接口
NativeNavigatorModule从继承RCTEventEmitter改为实现RCTTurboModule协议IosPlayerAdapter.ts从NativeModules改为TurboModuleRegistry
改动量跟 Android 端类似,核心工作在原生模块的改造上,JS 层和业务层几乎不用动。
11. 深入底层:线程模型
11.1 三个核心线程
React Native 运行时有三个主要线程协同工作:
graph TD
subgraph JSThread["JS 线程"]
J1["执行 React 组件逻辑"]
J2["处理事件回调"]
J3["调用 Turbo Modules"]
end
subgraph BGThread["后台线程(可多个)"]
B1["Yoga 布局计算"]
B2["网络请求"]
B3["图片解码"]
end
subgraph UIThread["UI 主线程"]
U1["创建/更新/删除原生 View"]
U2["处理触摸事件"]
U3["屏幕渲染(60fps)"]
end
JSThread -->|"渲染指令"| BGThread
BGThread -->|"布局结果"| UIThread
UIThread -->|"触摸事件"| JSThread
11.2 为什么要分线程
如果所有事情都在一个线程上做,JS 执行一段复杂逻辑时,界面就会卡住不响应触摸。分线程之后:
- JS 线程在后台计算,不阻塞界面
- UI 线程专注渲染,保持 60fps 流畅
- 布局计算在后台线程,不影响 JS 也不影响 UI
11.3 线程间通信
早期版本中,线程间通过 Bridge 异步通信,消息要排队。现在的版本中:
- JS 线程和 UI 线程可以通过 JSI 同步通信(Fabric 的同步渲染就依赖这个能力)
- 但大部分场景仍然是异步的,避免互相阻塞
- Fabric 会根据优先级决定是同步还是异步:用户正在交互的部分用同步,后台更新用异步
12. 深入底层:React Native 启动流程
一个 RN App 从点击图标到显示界面,经历以下步骤:
graph TD
A["用户点击 App 图标"] --> B["Native 层启动<br/>(iOS: AppDelegate / Android: MainActivity)"]
B --> C["初始化 Hermes 引擎"]
C --> D["加载 JS Bundle(字节码)"]
D --> E["执行 AppRegistry.registerComponent()"]
E --> F["React 开始渲染组件树"]
F --> G["Yoga 计算布局"]
G --> H["Fabric 创建原生 View"]
H --> I["屏幕显示界面"]
各阶段耗时分布(典型值):
| 阶段 | 耗时 | 说明 |
|---|---|---|
| Native 启动 | ~100ms | 创建 Activity/ViewController |
| Hermes 初始化 | ~50ms | 创建 JS 运行环境 |
| 加载 JS Bundle | ~100ms | 读取 .hbc 字节码文件 |
| JS 执行 | ~200ms | 执行组件注册和初始渲染逻辑 |
| 布局 + 渲染 | ~50ms | Yoga 计算 + 创建原生 View |
| 总计 | ~500ms | 从点击到首屏显示 |
Hermes 的字节码预编译在这里贡献最大——如果用传统 JS 引擎,"加载 JS Bundle"这一步可能要 300-500ms(因为要现场解析和编译),Hermes 把它压缩到了 100ms 左右。
13. 早期版本 vs 当前版本总结
graph LR
subgraph Old["早期版本(0.68 之前)"]
OJS["JS 线程"] -->|"JSON Bridge(异步)"| ONative["Native 线程"]
OJS -->|"JSON Bridge"| ORender["UI 渲染"]
end
subgraph New["新架构"]
NJS["JS 线程"] -->|"JSI(可同步)"| NNative["Native 代码"]
NJS -->|"Fabric + JSI"| NRender["UI 渲染"]
end
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| JS ↔ Native 通信 | JSON Bridge(异步,有序列化开销) | JSI(可同步,无序列化) |
| 原生模块加载 | 启动时全部初始化 | 懒加载(Turbo Modules) |
| 渲染器 | 异步渲染,可能白屏 | Fabric,支持同步渲染和优先级 |
| 布局计算 | 固定在 Shadow 线程 | 灵活,可在任意线程 |
| 类型安全 | 无,运行时才发现类型错误 | CodeGen 编译期检查 |
| JS 引擎 | JavaScriptCore | Hermes(字节码预编译) |
| 启动速度 | 较慢(~1-2s) | 较快(~500ms) |
| 内存占用 | 较高 | 降低 30%-50% |
新架构的核心思路就一句话:把中间的"翻译环节"尽可能去掉,让 JS 和 Native 直接对话。