React Native 底层原理

4 阅读12分钟

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 在中间做翻译:

  1. 你写 <View style={{backgroundColor: 'red'}}>
  2. 在 iOS 上,翻译成 UIView 并设置红色背景
  3. 在 Android 上,翻译成 android.view.View 并设置红色背景
  4. 在 Web 上,翻译成 <div style="background-color: red">

image.png

屏幕上显示的按钮、文字、输入框,都是各平台真正的原生组件,不是模拟出来的,所以外观和交互体验跟原生 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 启动快的根本原因。

image.png

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)

这个过程的瓶颈:

  1. 每次通信都要做 JSON 序列化/反序列化(把对象转成字符串再转回来),数据量大时很慢
  2. Bridge 是异步队列,消息要排队,高频调用时会堆积
  3. 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

image.png

7.3 JSI HostObject 机制

JSI 的核心概念是 HostObject——一个由 C++ 实现、但可以在 JS 中像普通对象一样使用的东西。

当 JS 访问 HostObject 的属性或调用方法时,实际上是在调用 C++ 的 getset 方法。这就是"直接通信"的本质: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 = .whitetext = "正在播放"
  • 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 我们项目当前的情况

配置项AndroidiOS
新架构(Fabric)✅ 已开启❌ 未开启
Hermes 引擎✅ 已开启❌ 未开启(用的 JavaScriptCore)
原生模块方式✅ Turbo Modules旧版 Native Modules

Android 端已经完成了 Turbo Modules 迁移,并接入了 CodeGen:

  • ReactVnspPlayerModule 继承 CodeGen 生成的 NativeVnspPlayerModuleSpec 抽象类,编译期保证接口一致
  • ReactVnspPlayerPackageReactPackage 改为继承 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.tsJS Spec 文件,用 TypeScript 定义模块所有方法的参数和返回值类型(同时作为 CodeGen 的输入源)
改写 AndroidPlayerAdapter.tsNativeModules.VnspPlayerModule 改为导入 Spec 文件获取模块实例
改写 ReactVnspPlayerModule.kt继承 CodeGen 生成的 NativeVnspPlayerModuleSpec 抽象类,编译期保证接口一致
改写 ReactVnspPlayerPackage.ktReactPackage 改为 TurboReactPackage,实现懒加载注册

PlayerService、WebPlayerAdapter、ReactVideoView、ReactVnspViewManager、MainApplication 都不需要改。

10.5 iOS 端后续迁移计划

iOS 端要迁移到 Turbo Modules,需要:

  1. 在 Podfile 中开启新架构(hermes_enabledfabric_enablednew_arch_enabled 改为 true
  2. 新增 JS Spec 文件,定义 NativeNavigatorModule 的接口
  3. NativeNavigatorModule 从继承 RCTEventEmitter 改为实现 RCTTurboModule 协议
  4. IosPlayerAdapter.tsNativeModules 改为 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执行组件注册和初始渲染逻辑
布局 + 渲染~50msYoga 计算 + 创建原生 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 引擎JavaScriptCoreHermes(字节码预编译)
启动速度较慢(~1-2s)较快(~500ms)
内存占用较高降低 30%-50%

新架构的核心思路就一句话:把中间的"翻译环节"尽可能去掉,让 JS 和 Native 直接对话。