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 结果要放在 useEffectonLayout

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

  • 老架构:
    • 点击响应慢、页面初始化慢、列表计算重,优先怀疑 JS 线程;
    • 原生转场卡顿、滚动掉帧、图片和复杂视图导致的卡顿,优先怀疑主线程/UI 线程;
      • 原生转场卡顿:通常指页面切换过程中由原生侧负责的过渡效果出现掉帧、顿挫或不跟手。
    • Native Module/TurboModule 的具体执行线程取决于模块实现,不应先假定它固定跑在某一条线程。
  • 新架构下:
    • 多数普通业务更新仍主要受 JS 线程影响,但高优先级交互在某些场景下可由 UI 线程同步推进,因此比旧架构更利于即时交互响应。
相关推荐
弓.长.2 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:react-native-shimmer-placeholder — 骨架屏组件
react native·react.js·harmonyos
早點睡3902 天前
ReactNative项目OpenHarmony三方库集成实战:react-native-calendar-events(读取不到日历里新增的事件,待排查)
javascript·react native·react.js
早點睡3903 天前
ReactNative项目OpenHarmony三方库集成实战:react-native-render-html
react native·react.js·html
早點睡3903 天前
ReactNative项目OpenHarmony三方库集成实战:react-native-sensors(设备传感器)
javascript·react native·react.js
早點睡3903 天前
ReactNative项目OpenHarmony三方库集成实战:react-native-snackbar
javascript·react native·react.js
吴敬悦4 天前
React Native 登陆 Meta Quest(专业译文)
react native
早點睡3905 天前
ReactNative项目OpenHarmony三方库集成实战:react-native-collapsible
javascript·react native·react.js
哈__5 天前
ReactNative项目OpenHarmony三方库集成实战:react-native-tts 语音播放
javascript·react native·react.js
早點睡3905 天前
ReactNative项目OpenHarmony三方库集成实战:react-native-background-timer
javascript·react native·react.js