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 倍栈空间:

// RCTCxxBridge.mm:434
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;

Native Module 线程为什么要独立

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

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

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

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

UI 线程为什么必须独立

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

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

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

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

不隔离的后果

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

三、各线程工作特点

JS 线程:CFRunLoop 驱动 + 批处理

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

// RCTMessageThread.mm
CFRunLoopPerformBlock(m_cfRunLoop, kCFRunLoopCommonModes, ^{ func(); });
CFRunLoopWakeUp(m_cfRunLoop);

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

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

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

  • GCD队列:指的是 iOS/macOS 里的 Grand Central Dispatch 任务调度队列。
// 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
// 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 线程同步推进,因此比旧架构更利于即时交互响应。
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐