RN线程模型

RN 用线程隔离解决了"JS 单线程 + UI主线程限制 + Native 耗时操作"三者之间的冲突,代价是所有跨线程操作都必须异步。理解这个,就理解了 RN 大多数性能问题的根源。


一、线程概览

在RN旧架构中,一般有3个线程:

  • UI / Main Thread
  • JS Thread
  • Shadow Thread

在RN新架构里,很多文章或分享会把运行时相关工作拆得更细,具体如下:

线程 源码名称 职责
JS 线程 "js" / "JavaScript" 执行所有 JavaScript 代码、处理 JS 回调
Native Module 线程 "native_modules" 执行 Native 模块方法调用
UI 线程(主线程) "main_ui" / dispatch_get_main_queue() 所有 View 更新、UI 操作
其他系统线程 Fabric / RuntimeScheduler 新架构引入,辅助渲染调度
源码依据:
  • JS 线程:ReactCommon/react/runtime/platform/ios/ReactCommon/RCTJSThreadManager.mm:13
  • Native Module 线程:ReactAndroid/.../bridge/queue/ReactQueueConfigurationSpec.kt:49
  • UI 线程:ReactAndroid/.../bridge/queue/MessageQueueThreadSpec.kt:23

二、为什么需要多线程

三件事性质完全不同,放在一起会互相阻塞甚至死锁。

JS 线程为什么要独立

JavaScript 引擎是单线程的,必须独占一个线程。如果和 UI 共用主线程,一段复杂 JS 逻辑跑起来,用户触摸屏幕没反应,动画会卡帧。

源码中 JS 线程被设置为最高优先级,还分配了 2 倍栈空间:

objc 复制代码
// RCTCxxBridge.mm:434
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;

Native Module 线程为什么要独立

Native 方法经常是耗时操作(文件 I/O、网络、加密),如果在 JS 线程同步调用,JS 线程会被整个卡住。源码里有明确警告:

objc 复制代码
// RCTBridgeModule.h:220
// WARNING: calling methods synchronously can have strong performance penalties
// and introduce threading-related bugs to your native modules.

所以绝大多数 Native 调用都是异步派发:

objc 复制代码
// RCTNativeModule.mm:90
dispatch_async(queue, block); // 派遣到 Native Module 的队列,不阻塞 JS

UI 线程为什么必须独立

iOS UIKit 强制要求所有 UI 操作只能在主线程执行,这是系统层面的约束。但 JS 不能同步调主线程,否则会死锁:

复制代码
JS 线程等主线程完成 UI 操作
    ↕ 同时
主线程等 JS 线程返回数据(用户交互触发)
    → 死锁

源码中有专门的断言宏强制检查线程归属:

objc 复制代码
// RCTAssert.h:106
#define RCTAssertMainQueue()    // UI 代码必须在主线程
#define RCTAssertNotMainQueue() // JS 代码不能在主线程

不隔离的后果

线程 不隔离会怎样
JS 线程 JS 逻辑跑起来 → UI 冻结,触摸无响应
Native Module 线程 耗时 Native 操作 → JS 卡死
UI 线程 JS 同步调用主线程 → 双方互等 → 死锁

三、各线程工作特点

JS 线程:CFRunLoop 驱动 + 批处理

驱动方式 :由 CFRunLoop 驱动,任务通过 CFRunLoopPerformBlock 入队,执行完唤醒 RunLoop 继续处理:

cpp 复制代码
// RCTMessageThread.mm
CFRunLoopPerformBlock(m_cfRunLoop, kCFRunLoopCommonModes, ^{ func(); });
CFRunLoopWakeUp(m_cfRunLoop);

批处理机制 :JS 调用 Native 不是逐条发送,而是批量收集后一次性通过 callFunctionReturnFlushedQueue 发出,由 isEndOfBatch 标记触发 onBatchComplete。目的是减少跨线程通信次数,类似浏览器的微任务批处理。

Native Module 线程:串行独立队列 + 异步回调

每个模块有独立的串行 GCD 队列:

  • GCD队列:指的是 iOS/macOS 里的 Grand Central Dispatch 任务调度队列。
objc 复制代码
// RCTModuleData.mm
_methodQueue = dispatch_queue_create("com.facebook.react.XxxQueue", DISPATCH_QUEUE_SERIAL);
  • 串行:保证单个模块的调用顺序
  • 独立:不同模块之间可以并行执行
  • 模块可重写 methodQueue 自定义队列

调用默认异步 ,结果通过 RCTResponseSenderBlock 回调传回 JS。只有极少数特殊模块(queue == RCTJSThread)才同步执行。

UI 线程:Shadow Tree 布局 + 批量 flush

两阶段设计:

  1. 布局计算(非主线程) :JS 描述的 UI 转成 ShadowView,由 Yoga 计算布局,生成 UIBlock 放入 _pendingUIBlocks
  2. UI 更新(主线程):CADisplayLink 每帧触发,批量 flush UIBlocks 到主线程更新真实 UIView
objc 复制代码
// RCTUIManager.mm
[_pendingUIBlocks addObject:block];          // 非主线程:收集变更
for (block in previousPendingUIBlocks) {     // 主线程:批量执行
    block(self.viewRegistry);
}

CADisplayLink 是整个系统的节拍器,每帧(~16.7ms)同时触发 JS 批处理和 UI flush,两者解耦但同频。


四、线程协作关系

复制代码
用户触摸 → 主线程 → 转发事件给 JS Thread
JS Thread → 批量调用 → Native Module Queues → callback 回 JS
JS Thread → UI 变更描述 → ShadowView 计算 → UIBlocks → 主线程更新 View
CADisplayLink 每帧驱动上述全流程

三个线程不直接通信,全部通过队列异步传递,这是线程安全的根本保障。
UI Thread / Main Thread
Native Module Queues(各模块独立串行队列)
JS Thread(CFRunLoop)
触发源
每帧触发 JS flush
每帧触发 flushUIBlocks
touch 事件
事件回调转发
dispatch_async
dispatch_async
callback / Promise
callback / Promise
UI 变更描述
addUIBlock
dispatch_async main
CADisplayLink

每帧 ~16.7ms
用户交互

touch / gesture
执行 JS 逻辑

setState / 事件处理
批处理队列

flushedQueue
模块A

dispatch_queue
模块B

dispatch_queue
ShadowView

Yoga 布局计算(非主线程)
_pendingUIBlocks

待提交队列
真实 UIView

更新渲染


五、新旧架构差异

旧架构(Bridge) 新架构(JSI/Fabric)
JS ↔ Native 通信 异步,必须跨线程序列化 JSI 直接调用,减少线程跳跃
Native Module 线程 iOS 每个模块可自定义 methodQueue 统一收敛
渲染线程 UIManager 在主线程 Fabric 独立调度

六、对实际开发的意义

1. 卡顿排查有方向

  • JS 线程繁忙 → 动画掉帧、交互延迟
  • 主线程繁忙 → 触摸无响应

2. 动画为什么要用 Reanimated

  • Animated 跑在 JS 线程,JS 一忙就掉帧
  • Reanimated 把动画逻辑移到 UI 线程,彻底绕开 JS,这是线程模型决定的根本差异

3. Native 回调为什么必须异步

  • 方法跑在模块自己的队列里,不在 JS 线程,结果只能通过 callback / Promise 回传

4. setState 之后为什么不立即生效

  • UI 更新是批量 flush 的,不是同步生效,依赖 UI 结果要放在 useEffect 或 onLayout 里

5. 线程和实际开发的关系

  • 老架构:
    • 点击响应慢、页面初始化慢、列表计算重,优先怀疑 JS 线程;
    • 原生转场卡顿、滚动掉帧、图片和复杂视图导致的卡顿,优先怀疑主线程/UI 线程;
      • 原生转场卡顿:通常指页面切换过程中由原生侧负责的过渡效果出现掉帧、顿挫或不跟手。
    • Native Module/TurboModule 的具体执行线程取决于模块实现,不应先假定它固定跑在某一条线程。
  • 新架构下:
    • 多数普通业务更新仍主要受 JS 线程影响,但高优先级交互在某些场景下可由 UI 线程同步推进,因此比旧架构更利于即时交互响应。
相关推荐
enjoywindstorm2 天前
移动端原生框架对比评测
react native
传奇开心果编程3 天前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
传奇开心果编程3 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程4 天前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
500844 天前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos
传奇开心果编程4 天前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
500845 天前
React Native for OpenHarmony 实战:三方库 react-native-volume-control 的鸿蒙化适配指南
javascript·react native·react.js·electron·harmonyos
500849 天前
React Native for OpenHarmony 实战:三方库 react-native-crypto-js 的鸿蒙化适配指南
javascript·react native·性能优化·electron·harmonyos
5008410 天前
React Native for OpenHarmony 实战:三方库 react-native-device-name 的鸿蒙化适配指南
深度学习·react native·react.js·机器学习·harmonyos
5008410 天前
React Native for OpenHarmony 实战:三方库 react-native-torch 的鸿蒙化适配指南
javascript·react native·react.js·harmonyos