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
两阶段设计:
- 布局计算(非主线程) :JS 描述的 UI 转成 ShadowView,由 Yoga 计算布局,生成 UIBlock 放入
_pendingUIBlocks - 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 线程同步推进,因此比旧架构更利于即时交互响应。