RN线程模型

张开发
2026/4/6 4:21:00 15 分钟阅读

分享文章

RN线程模型
RN 用线程隔离解决了JS 单线程 UI主线程限制 Native 耗时操作三者之间的冲突代价是所有跨线程操作都必须异步。理解这个就理解了 RN 大多数性能问题的根源。一、线程概览在RN旧架构中一般有3个线程UI / Main ThreadJS ThreadShadow 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:13Native Module 线程ReactAndroid/.../bridge/queue/ReactQueueConfigurationSpec.kt:49UI 线程ReactAndroid/.../bridge/queue/MessageQueueThreadSpec.kt:23二、为什么需要多线程三件事性质完全不同放在一起会互相阻塞甚至死锁。JS 线程为什么要独立JavaScript 引擎是单线程的必须独占一个线程。如果和 UI 共用主线程一段复杂 JS 逻辑跑起来用户触摸屏幕没反应动画会卡帧。源码中 JS 线程被设置为最高优先级还分配了 2 倍栈空间// RCTCxxBridge.mm:434_jsThread.qualityOfServiceNSOperationQualityOfServiceUserInteractive;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:90dispatch_async(queue,block);// 派遣到 Native Module 的队列不阻塞 JSUI 线程为什么必须独立iOS UIKit 强制要求所有 UI 操作只能在主线程执行这是系统层面的约束。但 JS 不能同步调主线程否则会死锁JS 线程等主线程完成 UI 操作 ↕ 同时 主线程等 JS 线程返回数据用户交互触发 → 死锁源码中有专门的断言宏强制检查线程归属// RCTAssert.h:106#defineRCTAssertMainQueue()// UI 代码必须在主线程#defineRCTAssertNotMainQueue()// JS 代码不能在主线程不隔离的后果线程不隔离会怎样JS 线程JS 逻辑跑起来 → UI 冻结触摸无响应Native Module 线程耗时 Native 操作 → JS 卡死UI 线程JS 同步调用主线程 → 双方互等 → 死锁三、各线程工作特点JS 线程CFRunLoop 驱动 批处理驱动方式由 CFRunLoop 驱动任务通过CFRunLoopPerformBlock入队执行完唤醒 RunLoop 继续处理// RCTMessageThread.mmCFRunLoopPerformBlock(m_cfRunLoop,kCFRunLoopCommonModes,^{func();});CFRunLoopWakeUp(m_cfRunLoop);批处理机制JS 调用 Native 不是逐条发送而是批量收集后一次性通过callFunctionReturnFlushedQueue发出由isEndOfBatch标记触发onBatchComplete。目的是减少跨线程通信次数类似浏览器的微任务批处理。Native Module 线程串行独立队列 异步回调每个模块有独立的串行 GCD 队列GCD队列指的是 iOS/macOS 里的Grand Central Dispatch任务调度队列。// RCTModuleData.mm_methodQueuedispatch_queue_create(com.facebook.react.XxxQueue,DISPATCH_QUEUE_SERIAL);串行保证单个模块的调用顺序独立不同模块之间可以并行执行模块可重写methodQueue自定义队列调用默认异步结果通过RCTResponseSenderBlock回调传回 JS。只有极少数特殊模块queue RCTJSThread才同步执行。UI 线程Shadow Tree 布局 批量 flush两阶段设计布局计算非主线程JS 描述的 UI 转成 ShadowView由 Yoga 计算布局生成 UIBlock 放入_pendingUIBlocksUI 更新主线程CADisplayLink 每帧触发批量 flush UIBlocks 到主线程更新真实 UIView// RCTUIManager.mm[_pendingUIBlocks addObject:block];// 非主线程收集变更for(blockinpreviousPendingUIBlocks){// 主线程批量执行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 ThreadNative Module Queues各模块独立串行队列JS ThreadCFRunLoop触发源每帧触发 JS flush每帧触发 flushUIBlockstouch 事件事件回调转发dispatch_asyncdispatch_asynccallback / Promisecallback / PromiseUI 变更描述addUIBlockdispatch_async mainCADisplayLink每帧 ~16.7ms用户交互touch / gesture执行 JS 逻辑setState / 事件处理批处理队列flushedQueue模块Adispatch_queue模块Bdispatch_queueShadowViewYoga 布局计算非主线程_pendingUIBlocks待提交队列真实 UIView更新渲染五、新旧架构差异旧架构Bridge新架构JSI/FabricJS ↔ Native 通信异步必须跨线程序列化JSI 直接调用减少线程跳跃Native Module 线程iOS 每个模块可自定义methodQueue统一收敛渲染线程UIManager 在主线程Fabric 独立调度六、对实际开发的意义1. 卡顿排查有方向JS 线程繁忙 → 动画掉帧、交互延迟主线程繁忙 → 触摸无响应2. 动画为什么要用 ReanimatedAnimated跑在 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 线程同步推进因此比旧架构更利于即时交互响应。

更多文章