4.JS Bundle 执行流程

JS bundle 从 RCTBridge.enqueueApplicationScript: 进入之后,怎么一步步被 RCTContextExecutor 喂进 JavaScriptCore 的 JSEvaluateScript,又怎么在执行完之后立刻通过 BatchedBridge.flushedQueue() 把 JS 侧排队的 native 调用拉回来给 Bridge。

整条链路:

rust 复制代码
RCTRootView
  -> [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:...]   <- 起点

RCTBridge.m
  -> setJavaScriptExecutor:                <- 注入 executor,并立刻 setUp
       ├─ 注册 Native Module 实例
       ├─ 构造 remoteModuleConfig + localModulesConfig
       └─ injectJSONText: 把 __fbBatchedBridgeConfig 写进 JS 全局
  -> enqueueApplicationScript:
       └─ [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^{
              [_javaScriptExecutor executeJSCall:@"BatchedBridge" method:@"flushedQueue" ...
                    callback:^{ [self _handleBuffer:json]; onComplete(error); }];
          }]

RCTContextExecutor.m
  -> +runRunLoopThread                      <- 进程级 JS 线程 RunLoop
  -> -init / -initWithJavaScriptThread:globalContextRef:
       ├─ dispatch_once 创建 com.facebook.React.JavaScript 线程
       └─ executeBlockOnJavaScriptQueue:
            ├─ JSContextGroupCreate / JSGlobalContextCreateInGroup
            └─ _addNativeHook: nativeLoggingHook / noop
  -> executeApplicationScript:sourceURL:onComplete:
       └─ executeBlockOnJavaScriptQueue:
            └─ JSEvaluateScript(_context, script, NULL, sourceURL, 0, &jsError)   <- 真正执行 JS 的一行
  -> executeJSCall:method:arguments:callback:
       └─ executeBlockOnJavaScriptQueue:
            ├─ 拼字符串 "require('%@').%@.apply(null, %@);"
            ├─ JSEvaluateScript(_context, ...)
            └─ JSValueCreateJSONString → RCTJSONParse → onComplete(objcValue, nil)

RCTBridge.m
  -> _handleBuffer:                         <- 终点

阅读文件

文件 作用
React/Base/RCTJavaScriptExecutor.h JS executor 协议
React/Base/RCTInvalidating.h invalidate / isValid 协议
React/Base/RCTBridge.h / .m Bridge 接口与实现
React/Executors/RCTContextExecutor.h / .m 默认 executor 声明与实现
React/Executors/RCTWebViewExecutor.h / .m 调试用 executor
Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h 远程调试 executor 声明
Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js JS 侧 BatchedBridge
JavaScriptCore.framework Apple 系统 JS 引擎

1. RCTContextExecutor.m 的整体结构

实例变量:

objectivec 复制代码
@implementation RCTContextExecutor
{
  JSGlobalContextRef _context;
  NSThread *_javaScriptThread;
}
变量 类型 作用
_context JSGlobalContextRef(C 指针) JavaScriptCore 的全局执行上下文,所有 JS 代码都在它里面跑
_javaScriptThread NSThread * RN 自建的 JS 专属线程,名字 com.facebook.React.JavaScript

文件级 C 函数(5 个,都是 static,文件外不可见):

函数 签名要点 用途
RCTNativeLoggingHook JSValueRef (...) Native hook,注入 JS 全局成 nativeLoggingHook()
RCTNoop JSValueRef (...) 空操作 hook,注入成 noop() 用于性能基准
RCTJSValueToNSString JSValueRef → NSString * JSC 字符串值转 Foundation 字符串
RCTJSValueToJSONString JSValueRef → NSString * JSC 任意值序列化成 JSON 字符串
RCTNSErrorFromJSError JSValueRef → NSError * JS 抛出的异常包装成 ObjC 错误

@interface 实现的协议方法:

方法 类型 何时被调
+runRunLoopThread 类方法 dispatch_once 里被 NSThread 当 selector 跑
-init 实例初始化 RCTRootView.loadBundle 里 alloc init
-initWithJavaScriptThread:globalContextRef: designated initializer 上面那个 -init 内部调它;测试时也可直接调它换 context
-executeBlockOnJavaScriptQueue: 内部分发 所有需要在 JS 线程跑的代码都过这里
-executeApplicationScript:sourceURL:onComplete: 协议方法 Bridge 第一阶段
-executeJSCall:method:arguments:callback: 协议方法 Bridge 第二阶段 + 后续所有 Native → JS
-injectJSONText:asGlobalObjectNamed:callback: 协议方法 Bridge setUp 注入模块表
-_addNativeHook:withName: 私有 注入 nativeLoggingHook / noop
-isValid / -invalidate / -dealloc RCTInvalidating reload / dealloc

RCTContextExecutor 真正「执行 JS」的逻辑集中在 executeApplicationScriptexecuteJSCall 两个方法,剩下全是线程、生命周期、JSC 资源管理的脚手架。主要精力花在这两个方法加 executeBlockOnJavaScriptQueue: 上。

2. RCTJavaScriptExecutor.h:Bridge 依赖的抽象协议

目标:理解 Bridge 为什么不是直接 import RCTContextExecutor.h,而是依赖一个抽象协议。

objectivec 复制代码
#import <JavaScriptCore/JavaScriptCore.h>

#import "RCTInvalidating.h"

typedef void (^RCTJavaScriptCompleteBlock)(NSError *error);
typedef void (^RCTJavaScriptCallback)(id json, NSError *error);

/**
 * Abstracts away a JavaScript execution context - we may be running code in a
 * web view (for debugging purposes), or may be running code in a `JSContext`.
 */
@protocol RCTJavaScriptExecutor <RCTInvalidating>

- (void)executeJSCall:(NSString *)name
               method:(NSString *)method
            arguments:(NSArray *)arguments
             callback:(RCTJavaScriptCallback)onComplete;

- (void)executeApplicationScript:(NSString *)script
                       sourceURL:(NSURL *)url
                      onComplete:(RCTJavaScriptCompleteBlock)onComplete;

- (void)injectJSONText:(NSString *)script
   asGlobalObjectNamed:(NSString *)objectName
              callback:(RCTJavaScriptCompleteBlock)onComplete;
@end
元素 含义
#import <JavaScriptCore/JavaScriptCore.h> 头文件层引入 JSC 类型(主要为下面 typedef)。尖括号 = 系统 Framework,不是 RN 仓库文件
RCTJavaScriptCompleteBlock void (^)(NSError *),只有错误、没有返回值------用于「启动 / 注入」这种只关心成功失败的场景
RCTJavaScriptCallback void (^)(id json, NSError *),带 JSON 返回值------用于「执行 JS 函数并收返回值」的场景
<RCTInvalidating> 协议继承协议:任何 executor 同时也是可销毁对象,必须实现 isValid / invalidate
executeJSCall:method:arguments:callback: 让 JS 跑一次 Module.method(args),把返回值 JSON 化交回来
executeApplicationScript:sourceURL:onComplete: 执行一整段 JS 源码字符串(典型用法:bundle)
injectJSONText:asGlobalObjectNamed:callback: 把一段 JSON 字符串 parse 成 JS 值,挂到全局对象上某个名字下

三个反常识细节:

  1. 协议只要求 3 个方法,不要求 setUp **/ setBridge: ****。 **这意味着 Bridge 不能假设 executor 知道自己------所有「模块表注入」都得通过 injectJSONText: 这条窄通道做。后面会看到 __fbBatchedBridgeConfig 就是这么塞进去的。
  2. executeJSCall:method: 入参是 module name + method name 两段,不是一个 "Module.method" **字符串。 **这跟 RCTBridge.enqueueJSCall: 入参形态恰好相反------enqueueJSCall:@"AppRegistry.runApplication" 是带点字符串,到 executor 这一层 Bridge 已经查表换成 module/method ID 数组,从不会调 executeJSCall:@"AppRegistry" method:@"runApplication" 这种形式。真正调 executeJSCall: 的地方只有两处:Bridge 自己的 _invokeAndProcessModule:enqueueApplicationScript: 完成后那一次 flushedQueue
  3. 返回值是 id json ,不是 id obj ** **这暗示一个核心约定:JS 和 Native 跨界传值只走 JSON,没有 JS 对象直接暴露给 Native 的可能。后面会看到 executor 真的在内部 JSValueCreateJSONString 一次。

这层抽象的价值:

arduino 复制代码
RCTBridge
  -> 依赖 RCTJavaScriptExecutor 协议
        ├─ 默认实现:RCTContextExecutor(JavaScriptCore in-process)
        ├─ 调试实现:RCTWebViewExecutor(藏在 UIWebView 里跑)
        └─ 远程实现:RCTWebSocketExecutor(通过 WebSocket 把 JS 跑在 Chrome / Safari)

Bridge 永远只调协议里那 3 个方法,所以 Cmd-D 切换 executor 时,Bridge 内部代码一行都不用改。

易混淆点------RCTInvalidating 协议的命名:

less 复制代码
@protocol RCTInvalidating <NSObject>
@property (nonatomic, assign, readonly, getter = isValid) BOOL valid;
- (void)invalidate;
@end

属性名是 valid,getter 是 isValid。这是 Cocoa 命名约定:BOOL 属性名不带 is,但 getter 加 is。读代码时 executor.valid[executor isValid] 是同一回事。

3. executor 候选

优先级 路径 执行 JS 的方式
1(实例级) _executorClass 指定 由业务代码 set 任意自定义 executor
2(进程级) _globalExecutorClass(Cmd-D) NSClassFromString(@"RCTWebSocketExecutor") WebSocket 把 JS 跑在 Chrome / Safari dev tools
3(默认) RCTContextExecutor React/Executors/RCTContextExecutor.m 进程内 JavaScriptCore
旁支 RCTWebViewExecutor React/Executors/RCTWebViewExecutor.m 隐藏的 UIWebView 里跑 JS

4. RCTContextExecutor.h

objectivec 复制代码
#import <JavaScriptCore/JavaScriptCore.h>

#import "RCTJavaScriptExecutor.h"

// TODO (#5906496): Might RCTJSCoreExecutor be a better name for this?

/**
 * Uses a JavaScriptCore context as the execution engine.
 */
@interface RCTContextExecutor : NSObject <RCTJavaScriptExecutor>

/**
 * Configures the executor to run JavaScript on a custom performer.
 * You probably don't want to use this; use -init instead.
 */
- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread
                        globalContextRef:(JSGlobalContextRef)context;

@end

JSGlobalContextRef 这个类型:

arduino 复制代码
typedef struct OpaqueJSContext *JSGlobalContextRef;

它是 JavaScriptCore C-API 的核心类型------一个不透明结构体指针。整个 JS 引擎的状态都封装在它指向的内存里:全局对象、所有变量、函数、闭包。你能对它做的操作有限但都关键:

API 作用
JSGlobalContextCreateInGroup 在某个 context group 里建一个新 context
JSGlobalContextRetain 引用计数 +1
JSGlobalContextRelease 引用计数 -1,为 0 时销毁 context
JSContextGetGlobalObject 拿到全局对象 this(JS 里的 window 类比)
JSEvaluateScript 执行 JS 字符串

所有 JSC 类型都用 Ref 后缀(JSValueRef / JSStringRef / JSObjectRef...),它们都是 C-API 的不透明指针,ARC 不管,必须手动 release。

这个 init 的「双模式」提示:initWithJavaScriptThread:globalContextRef: 两个参数都可以传「非默认」。javaScriptThread 可以让 JS 跑在你指定的线程上(测试常用);context 可以复用一个已有的 JSGlobalContextRef,让多个 executor 共享同一个 JS 上下文(WebKit / WebView 里常见的优化模式)。业务代码 -init 总是传 default + NULL,但测试里这两个口子很关键。

5. -init 与 JS 线程创建

objectivec 复制代码
- (instancetype)init
{
  static NSThread *javaScriptThread;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    // All JS is single threaded, so a serial queue is our only option.
    javaScriptThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoopThread) object:nil];
    [javaScriptThread setName:@"com.facebook.React.JavaScript"];
    [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]];
    [javaScriptThread start];
  });

  return [self initWithJavaScriptThread:javaScriptThread globalContextRef:NULL];
}
做的事 关键认知
static NSThread *javaScriptThread; 文件级静态变量,进程内唯一 所有 executor 共享同一条 JS 线程------reload 后新 executor 还在同一条线程上跑
static dispatch_once_t onceToken; 配合 dispatch_once 的标志位 本质是 long,全零表示「还没跑过」
dispatch_once(&onceToken, ^{...}) 保证 block 在整个进程里恰好跑一次,线程安全 比手写双检锁好太多,用 atomic + memory barrier 实现
initWithTarget:[self class] selector:@selector(runRunLoopThread) 用类方法当线程入口 注意是 [self class] 而不是字面量,子类可覆写 runRunLoopThread
setName:@"com.facebook.React.JavaScript" NSThread 名字 Xcode 暂停 App 时左侧线程列表能看到它
setThreadPriority:...mainThread...threadPriority JS 线程优先级等同主线程 JS 是渲染相关计算,与主线程对等优先级保证响应度
return [self initWithJavaScriptThread:... globalContextRef:NULL] 委托给指定初始化器,context 传 NULL 表示「自己建一个新的」 ---

为什么必须用单独线程:

设计点 解释
不能跑在主线程 iOS 主线程负责 UIKit 布局/渲染/事件分发,JS 执行可能耗时几十毫秒,会卡 UI
不能用串行 dispatch_queue_t JavaScriptCore 早期版本硬性要求「JS 必须始终在同一线程」(GC、JIT 都假设了线程亲和),dispatch queue 不保证每次 block 跑在同一个工作线程
必须有 RunLoop RN 用 performSelector:onThread: 投递任务到 JS 线程,这依赖目标线程的 RunLoop 还在跑

所以这条 JS 线程的形态是:NSThread + 自定义 selector + 强制 RunLoop 常驻,不能用更简洁的 dispatch_queue_create

6. +runRunLoopThread

理解 JS 线程为什么必须挂一个 RunLoop,而且这个 RunLoop 还得「防自旋」。

objectivec 复制代码
+ (void)runRunLoopThread
{
  // TODO (#5906496): Investigate exactly what this does and why
  @autoreleasepool {
    // copy thread name to pthread name
    pthread_setname_np([[[NSThread currentThread] name] UTF8String]);

    // Set up a dummy runloop source to avoid spinning
    CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
    CFRelease(noSpinSource);

    // run the run loop
    while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, [[NSDate distantFuture] timeIntervalSinceReferenceDate], NO)) {
      RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad.
    }
  }
}
做的事 为什么
@autoreleasepool { ... } 整段包在自动释放池里 线程入口必须自己建 autorelease pool;主线程是 UIKit 帮你建,子线程要手写
pthread_setname_np(...) 把 NSThread 名字同步到 POSIX pthread 名字 Xcode 看 NSThread 名,Instruments / 系统级调试看 pthread 名,两边都要设
构造空 CFRunLoopSource RunLoop 没有 source 时会立刻退出 加一个永远不被触发的 dummy source,让 RunLoop「有事可等」
CFRunLoopAddSource(...) 把 source 加到 kCFRunLoopDefaultMode JS 线程的 RunLoop 跑在 default mode(不是 tracking mode)
CFRelease(noSpinSource) source 引用计数 -1(已被 RunLoop 持有) CF 引用计数是手动的,ARC 不管
while (kCFRunLoopRunStopped != CFRunLoopRunInMode(...)) 在 RunLoop 上等待,直到外部 stop 才退 第二个参数是超时时间,传 distantFuture 表示「永远等」
RCTAssert(NO, ...) RunLoop 因别的原因退出就立刻断言 注释说 "runloop spun. that's bad."------RunLoop 不该自然结束

防自旋的原理:如果 RunLoop 启动时没有任何 source / timer / observer,它会立刻返回(CFRunLoop 的设计)。while 循环里再次 CFRunLoopRunInMode 又立刻返回,CPU 就会 100%。加一个 dummy source 让 RunLoop 进入「等待事件」状态,CPU 占用归零------这就是 "avoid spinning"。

JS 线程的生命周期:

objectivec 复制代码
进程启动
  ▼
首次 [[RCTContextExecutor alloc] init]
  ▼
dispatch_once
  ├─ NSThread alloc / setName / start
  ├─ 跳到 JS 线程 → +runRunLoopThread 开始跑
  │     ├─ 加空 source
  │     └─ CFRunLoopRunInMode 进入等待
  ▼
继续主线程:initWithJavaScriptThread:globalContextRef:
  └─ executeBlockOnJavaScriptQueue:^{ ... 创建 context 等 }
        └─ performSelector:onThread: 把 block 投到 JS 线程
              ▼
        JS 线程 RunLoop 收到 source 触发
              └─ 执行 block:JSContextGroupCreate / JSGlobalContextCreateInGroup / _addNativeHook
                    └─ block 跑完,RunLoop 再次进入等待

后续:每一次 Native → JS 的调用都走同一条 RunLoop

7. initWithJavaScriptThread:globalContextRef:

搞清楚 JavaScriptCore 的执行上下文怎么创建出来,以及为什么这一步必须切到 JS 线程做。

objectivec 复制代码
- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread
                        globalContextRef:(JSGlobalContextRef)context
{
  if ((self = [super init])) {
    _javaScriptThread = javaScriptThread;
    [self executeBlockOnJavaScriptQueue: ^{
      // Assumes that no other JS tasks are scheduled before.
      if (context) {
        _context = JSGlobalContextRetain(context);
      } else {
        JSContextGroupRef group = JSContextGroupCreate();
        _context = JSGlobalContextCreateInGroup(group, NULL);
#if FB_JSC_HACK
        JSContextGroupBindToCurrentThread(group);
#endif
        JSContextGroupRelease(group);
      }

      [self _addNativeHook:RCTNativeLoggingHook withName:"nativeLoggingHook"];
      [self _addNativeHook:RCTNoop withName:"noop"];
    }];
  }

  return self;
}
做的事 关键认知
if ((self = [super init])) 标准 designated init 模式 跑 super init,保证父类 ivar 初始化
_javaScriptThread = javaScriptThread; 保存外部传入的 JS 线程 executeBlockOnJavaScriptQueue: 要靠它做线程判断
[self executeBlockOnJavaScriptQueue: ^{...}] 把 context 创建切到 JS 线程做 JSC 要求 context 在哪条线程创建,所有后续 JS 调用就锁死在那条线程。主线程建、JS 线程跑 = 崩
注释 "Assumes that no other JS tasks are scheduled before." 文档级断言 init 必须发生在任何 executor 使用之前
if (context) _context = JSGlobalContextRetain(context); 外部传了 context 就复用,引用计数 +1 测试 / 多 root view 共享 JS 上下文场景
JSContextGroupCreate() 创建一个 context group group 是 context 的隔离边界;同一 group 内的 context 可互相传值
JSGlobalContextCreateInGroup(group, NULL) 在 group 里建 global context 第二个参数是「全局对象的 class」,NULL 表示默认(空对象)
JSContextGroupRelease(group) group 引用计数 -1 context 内部已 retain 了 group;这里 release 掉外部那一份,避免泄漏
_addNativeHook:RCTNativeLoggingHook ... 注入 native 日志钩子 见本节末
_addNativeHook:RCTNoop ... 注入空操作钩子 性能基准用,调用它的开销 ≈ 一次 JSC 函数调用最小成本

为什么必须切线程:JSGlobalContextCreateInGroup 会在当前线程上初始化 JSC 的 GC 根、JIT 缓冲、stack walker 起点。一旦初始化,这个 context 后续所有调用都隐式假设跑在同一条线程。如果在主线程建、在 JS 线程跑,遇到 GC 时 JSC 会去主线程找 stack 根,找不到就崩。所以 executeBlockOnJavaScriptQueue: 这一行不是「为了性能而异步」,而是「为了不崩而必须切线程」。

_addNativeHook:------给 JS 全局挂 native 函数:

scss 复制代码
- (void)_addNativeHook:(JSObjectCallAsFunctionCallback)hook withName:(const char *)name
{
  JSObjectRef globalObject = JSContextGetGlobalObject(_context);

  JSStringRef JSName = JSStringCreateWithUTF8CString(name);
  JSObjectSetProperty(_context, globalObject, JSName, JSObjectMakeFunctionWithCallback(_context, JSName, hook), kJSPropertyAttributeNone, NULL);
  JSStringRelease(JSName);
}
步骤 API 干什么
1 JSContextGetGlobalObject(_context) 拿到 JS 全局对象(JS 里写 nativeLoggingHook(...) 就是查这个对象的属性)
2 JSStringCreateWithUTF8CString(name) C 字符串 "nativeLoggingHook" 转成 JSC 字符串
3 JSObjectMakeFunctionWithCallback(_context, JSName, hook) 把 C 函数指针 hook 包装成 JS function 对象
4 JSObjectSetProperty(_context, globalObject, JSName, jsFunction, kJSPropertyAttributeNone, NULL) 把这个 JS function 挂到 global object 上某个名字下
5 JSStringRelease(JSName) 释放第 2 步创建的 JSC 字符串

8. executeBlockOnJavaScriptQueue:

objectivec 复制代码
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
  if ([NSThread currentThread] != _javaScriptThread) {
    [self performSelector:@selector(executeBlockOnJavaScriptQueue:)
                 onThread:_javaScriptThread withObject:block waitUntilDone:NO];
  } else {
    block();
  }
}

performSelector:onThread: 怎么工作(Cocoa 的「跨线程方法调用」原生机制):

  1. 把「调用方法 + 参数」打包成一个 NSPortMessage
  2. enqueue 到目标线程 RunLoop 的 input source;
  3. 触发 source signal,让目标线程 RunLoop 醒来;
  4. 目标线程 RunLoop 在 default mode 的下一次 iteration 里 dequeue 并执行。

关键点:目标线程必须在 default mode 上跑 RunLoop(回到第 6 节为什么要 kCFRunLoopDefaultMode),且 RunLoop 不能已退出(回到第 6 节为什么要 while 循环防 RunLoop 自然结束)。

waitUntilDone:NO 的语义:

makefile 复制代码
waitUntilDone:YES → 当前线程阻塞,等目标线程跑完 block 才返回(同步)
waitUntilDone:NO  → 只投递不等待,立刻返回(异步)

这里用 NO------所有 JS 调用都是 fire-and-forget。Bridge 调 executeApplicationScript: 之后立刻返回到 NSURLSession 的 completion block 继续往下跑,不阻塞。等 JS 真的执行完,executor 通过 onComplete block 主动回调 Bridge。这是 RN 异步通信模型的根:Native → JS 永远是非阻塞 + 回调形式。

易混淆点------withObject: 只能传一个:performSelector:onThread:withObject:waitUntilDone:withObject: 只能传一个 id 参数。这里 selector 是 executeBlockOnJavaScriptQueue:,刚好一个参数(block 本身)------这是 RN 选它作为 selector 的原因之一。要跨线程调多参数方法,得先打包成 NSDictionary 或 block。RN 干脆把所有 JS 操作都包成 block,再统一通过这个分发器跳线程,这是 Cocoa 时代典型的「线程跳板」模式。

分发图:

ini 复制代码
任意线程
   ▼
executeBlockOnJavaScriptQueue:
   ├─ 当前已是 JS 线程 → 同步执行 block
   └─ 当前是别的线程
         ▼
      performSelector:onThread:_javaScriptThread waitUntilDone:NO
         ▼
      JS 线程 RunLoop 下次 iteration 时 dequeue
         ▼
      再次进入 executeBlockOnJavaScriptQueue:(此时 currentThread == _javaScriptThread)
         └─ 走 else 分支同步执行 block

第二次进入时 currentThread == _javaScriptThread,走 else------这是一个自递归的分发器,但只递归一次。

对照------RCTWebViewExecutor 怎么实现同名方法:

scss 复制代码
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
  dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC);
  dispatch_after(when, dispatch_get_main_queue(), ^{
    RCTAssertMainThread();
    block();
  });
}

WebView 是 UIKit 类,必须在主线程操作。所以它的 executeBlockOnJavaScriptQueue: 不是切到自建线程,而是切到主线程,并 dispatch_after 延迟 1ms 规避 UIWebView 早期版本的事件循环死锁(注释提到 WebKit bug 125746)。两个 executor 实现同一个方法、走完全不同的线程。

9. RCTBridge.setJavaScriptExecutor: + setUp

目标:搞清楚 bundle 还没开始跑之前,Bridge 已经偷偷塞了什么进 JS 全局。这是当前 commit 里最容易被略过、却决定 BatchedBridge 能不能工作的关键一步。

ini 复制代码
- (void)setJavaScriptExecutor:(id<RCTJavaScriptExecutor>)executor
{
  _javaScriptExecutor = executor;
  _latestJSExecutor = _javaScriptExecutor;
  [self setUp];
}

- (void)setUp
{
  // Register passed-in module instances
  NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
  for (id<RCTBridgeModule> module in _moduleProvider ? _moduleProvider() : nil) {
    preregisteredModules[RCTModuleNameForClass([module class])] = module;
  }

  // Instantiate modules
  _modulesByID = [[RCTSparseArray alloc] init];
  NSMutableDictionary *modulesByName = [preregisteredModules mutableCopy];
  [RCTBridgeModuleClassesByModuleID() enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
    NSString *moduleName = RCTModuleNamesByID[moduleID];
    // Check if module instance has already been registered for this name
    if ((_modulesByID[moduleID] = modulesByName[moduleName])) {
      // ...skip 注册检查(第 5 篇展开)...
    } else {
      // Module name hasn't been used before, so go ahead and instantiate
      id<RCTBridgeModule> module = [[moduleClass alloc] init];
      if (module) {
        _modulesByID[moduleID] = modulesByName[moduleName] = module;
      }
    }
  }];

  // Store modules
  _modulesByName = [modulesByName copy];

  // Set bridge
  for (id<RCTBridgeModule> module in _modulesByName.allValues) {
    if ([module respondsToSelector:@selector(setBridge:)]) {
      module.bridge = self;
    }
  }

  // Inject module data into JS context
  NSString *configJSON = RCTJSONStringify(@{
                                            @"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName),
                                            @"localModulesConfig": RCTLocalModulesConfig()
                                            }, NULL);
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(id err) {
    dispatch_semaphore_signal(semaphore);
  }];

  if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) {
    RCTLogError(@"JavaScriptExecutor took too long to inject JSON object");
  }
}

setJavaScriptExecutor: 做的 3 件事:保存到实例变量 _javaScriptExecutor;同步到文件级 static _latestJSExecutor(用于 +[RCTBridge log:level:] 这种类方法在没有 bridge 实例时也能找到一个有效 executor);调 setUp 触发模块注册 + 配置注入。

setUp 的 5 步:

步骤 做什么
1 收集 _moduleProvider 预注册的模块实例
2 遍历 RCTBridgeModuleClassesByModuleID()(runtime 扫描出的全部 RCTBridgeModule),实例化模块
3 _modulesByName = [modulesByName copy]
4 给每个模块 setBridge:self
5 构造 __fbBatchedBridgeConfig JSON,通过 injectJSONText: 塞进 JS 全局

__fbBatchedBridgeConfig 的形状

RCTRemoteModulesConfigRCTLocalModulesConfigRCTBridge.m 上半部)构造的字典形状:

json 复制代码
{
  "remoteModuleConfig": {
    "RCTUIManager": {
      "moduleID": 0,
      "methods": {
        "createView":      { "methodID": 0, "type": "remote" },
        "updateView":      { "methodID": 1, "type": "remote" },
        "manageChildren":  { "methodID": 2, "type": "remote" }
      },
      "constants": { }
    },
    "RCTSourceCode": {
      "moduleID": 1,
      "methods": { }
    }
  },
  "localModulesConfig": {
    "AppRegistry": {
      "moduleID": 0,
      "methods": {
        "runApplication": { "methodID": 0, "type": "local" }
      }
    },
    "ReactIOS": {
      "moduleID": 1,
      "methods": {
        "unmountComponentAtNodeAndRemoveContainer": { "methodID": 0, "type": "local" }
      }
    }
  }
}
字段 含义
remoteModuleConfig Native 模块对外暴露的清单。JS 侧 NativeModules.RCTUIManager.createView(...) 调用时转成 [0, 0, args] 三元组,通过 BatchedBridge 队列发回 Native
localModulesConfig JS 模块对 Native 暴露的清单。来自每个 Native 类的 +JSMethods
moduleID / methodID 数字 ID。JS 和 Native 通信全部走数字而非字符串,因为 JSON 里数字比字符串紧凑

这就是为什么 BatchedBridge 队列里看到的是 [[moduleID0, ...], [methodID0, ...], [params0, ...]] 三个并行数组------它要的字段 ID 全都来自这份注入的配置。

JS 侧怎么消费这个全局

Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js

javascript 复制代码
'use strict';

var BatchedBridgeFactory = require('BatchedBridgeFactory');
var MessageQueue = require('MessageQueue');

var remoteModulesConfig = __fbBatchedBridgeConfig.remoteModuleConfig;
var localModulesConfig = __fbBatchedBridgeConfig.localModulesConfig;

var BatchedBridge = BatchedBridgeFactory.create(
  MessageQueue,
  remoteModulesConfig,
  localModulesConfig
);

注意第 5、6 行:模块加载时直接读 __fbBatchedBridgeConfig 全局,所以这个全局必须在 require('BatchedBridge') 第一次执行之前就已注入。整条时序:

markdown 复制代码
1. Native 创建 bridge → setJavaScriptExecutor: → setUp
2. setUp 内部 injectJSONText: 把 __fbBatchedBridgeConfig 塞进 JS 全局
3. setUp 通过 dispatch_semaphore_wait 等 inject 完成
4. Native 调 enqueueApplicationScript: → executor.executeApplicationScript:
5. JS bundle 开始跑 → require('BatchedBridge') → 读 __fbBatchedBridgeConfig → OK
6. bundle 跑完
7. Native 调 executor.executeJSCall:@"BatchedBridge" method:@"flushedQueue"
8. BatchedBridge.flushedQueue() 在 JS 里返回排队的 native 调用三元组
9. Native _handleBuffer: 解开

如果第 2 步失败或第 3 步没等到,第 5 步 require 时 __fbBatchedBridgeConfig 会是 undefined,整个 RN 直接挂在加载阶段。

dispatch_semaphore_wait 是同步阻塞点

ini 复制代码
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[_javaScriptExecutor injectJSONText:... callback:^(id err) {
  dispatch_semaphore_signal(semaphore);
}];

if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) {
  RCTLogError(@"JavaScriptExecutor took too long to inject JSON object");
}
函数 作用
dispatch_semaphore_create(0) 创建初始计数为 0 的信号量。wait 时计数 > 0 立刻减 1 返回,否则阻塞
dispatch_semaphore_signal(semaphore) 计数 +1,唤醒一个等待线程
dispatch_semaphore_wait(semaphore, timeout) 阻塞直到 signal,或超时
超时 NSEC_PER_SEC 1 秒。超过就 RCTLogError 但继续往下走------不崩

这是 RN 在异步 executor 接口上强行加同步:injectJSONText: 本身是异步的(带 callback),但 Bridge 这里必须等它完成,否则后续 bundle 执行会读不到全局变量。

10. injectJSONText:asGlobalObjectNamed:callback:

搞清楚「往 JS 全局塞一个对象」经过哪几个 JSC API。

objectivec 复制代码
- (void)injectJSONText:(NSString *)script
   asGlobalObjectNamed:(NSString *)objectName
              callback:(RCTJavaScriptCompleteBlock)onComplete
{
  RCTAssert(onComplete != nil, @"onComplete block should not be nil");
#if DEBUG
  RCTAssert(RCTJSONParse(script, NULL) != nil, @"%@ wasn't valid JSON!", script);
#endif

  [self executeBlockOnJavaScriptQueue:^{
    JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
    JSValueRef valueToInject = JSValueMakeFromJSONString(_context, execJSString);
    JSStringRelease(execJSString);

    if (!valueToInject) {
      NSString *errorDesc = [NSString stringWithFormat:@"Can't make JSON value from script '%@'", script];
      RCTLogError(@"%@", errorDesc);

      NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}];
      onComplete(error);
      return;
    }

    JSObjectRef globalObject = JSContextGetGlobalObject(_context);

    JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)objectName);
    JSObjectSetProperty(_context, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL);
    JSStringRelease(JSName);
    onComplete(nil);
  }];
}

等价的 JS 代码就一句:

ini 复制代码
global[objectName] = JSON.parse(script);

只是为了避免 JS 字符串拼接和额外的 eval,RN 走 C-API 直接做。JSValueMakeFromJSONString 是 JSC 在 C 层提供的 JSON.parse,执行不依赖任何 JS 代码,所以 inject 可以发生在 bundle 加载之前。

关键事实------__bridge vs __bridge_transfer

ini 复制代码
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);

这里写 __bridge------告诉 ARC 这个指针变成 CF 类型时不要转移所有权,script 还是被 ARC 持有,方法结束后正常释放。反过来从 CF 拿回 NS 时常用 __bridge_transfer

ini 复制代码
NSString *str = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string);

JSStringCopyCFString 返回 owned CF 对象,__bridge_transfer 把所有权交给 ARC,ARC 在 str 不再被引用时自动 release。这两个修饰符是 ObjC 跟 CoreFoundation 互操作的关键,看 JSC 代码必定出现。

JSObjectSetProperty 的第 5 个参数(属性 attribute)可选值:

含义
kJSPropertyAttributeNone 普通属性,可写、可枚举、可删除
kJSPropertyAttributeReadOnly 只读
kJSPropertyAttributeDontEnum 不出现在 for...in 枚举里
kJSPropertyAttributeDontDelete 不能用 delete 删除

11. RCTBridge.enqueueApplicationScript:

看 Bridge 怎么把 bundle 交给 executor,并理解为什么 bundle 跑完立刻要调 flushedQueue

objectivec 复制代码
- (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
  RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");
  [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {
    if (scriptLoadError) {
      onComplete(scriptLoadError);
      return;
    }

    [_javaScriptExecutor executeJSCall:@"BatchedBridge"
                                method:@"flushedQueue"
                             arguments:@[]
                              callback:^(id json, NSError *error) {
                                [self _handleBuffer:json];
                                onComplete(error);
                              }];
  }];
}
阶段 调用 在哪个线程跑
阶段 1 executeApplicationScript: 跑整段 bundle JS 线程(executor 内部 dispatch 过去)
阶段 2 executeJSCall:@"BatchedBridge" method:@"flushedQueue" JS 线程

成功路径走完 2 阶段,失败路径(脚本加载错误)只走 1 阶段。

**阶段 2 为什么是「立刻」: **bundle 执行完不代表 JS 没事做了------加载过程中可能已经触发了一堆 native 调用(require 链路上各模块初始化会调 native)。这些 native 调用在 JS 侧不是直接发出,而是先入 BatchedBridge 队列;队列里堆着 [moduleIDs, methodIDs, params, callbackIDs, returnValues, flushDateMillis] 6 个并行数组。Native 必须主动调一次 flushedQueue 把队列取走,否则 native 调用永远不会发生。

scss 复制代码
bundle 执行
  ├─ require('AppRegistry') → 内部可能调 NativeModules.X.y(...) → 入队 [X, y, args]
  ├─ require('UIManager') → 同上
  └─ ...
bundle 执行完
  ▼
flushedQueue() ← Native 主动问
  └─ JS 返回 [[0, 1, 2, ...], [3, 4, 5, ...], [[...], [...], ...], ...]
                ↑ moduleIDs    ↑ methodIDs    ↑ paramsArrays
  ▼
Native _handleBuffer: 解开,遍历分发到对应 Native 模块的方法

这就是 BatchedBridge 名字的由来------bundle 加载 + 后续每一次 JS 主动 push,都把多次 native 调用打包成一次跨界 batch,减少 JS↔Native 跨界开销。

flushedQueue **是 JS 侧 BatchedBridge 模块的方法。 **BatchedBridge.js 通过 BatchedBridgeFactory.create(MessageQueue, ...) 返回一个有 flushedQueue 方法的对象。后面会看到 executor 拼出的字符串:

javascript 复制代码
require('BatchedBridge').flushedQueue.apply(null, []);

如果 bundle 加载失败,scriptLoadError 分支早返回,flushedQueue 不会被调------这是为什么 if (scriptLoadError) { onComplete(scriptLoadError); return; } 必须严格守住。

_handleBuffer: 入参 buffer 就是 flushedQueue 返回的 6 字段数组,它内部会:① 校验 buffer 是 NSArray 且有 6 个字段;② 取出 moduleIDs / methodIDs / paramsArrays;③ 遍历调 _handleRequestNumber:moduleID:methodID:params:;④ 在 shadowQueue 上做 native 方法的 NSInvocation 调度。

12. executeApplicationScript:sourceURL:onComplete:

找到 bundle 真正进入 JS 引擎的那一行。

objectivec 复制代码
- (void)executeApplicationScript:(NSString *)script
                       sourceURL:(NSURL *)url
                      onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
  RCTAssert(url != nil, @"url should not be nil");
  RCTAssert(onComplete != nil, @"onComplete block should not be nil");
  [self executeBlockOnJavaScriptQueue:^{
    JSValueRef jsError = NULL;
    JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
    JSStringRef sourceURL = JSStringCreateWithCFString((__bridge CFStringRef)url.absoluteString);
    JSValueRef result = JSEvaluateScript(_context, execJSString, NULL, sourceURL, 0, &jsError);
    JSStringRelease(sourceURL);
    JSStringRelease(execJSString);

    NSError *error;
    if (!result) {
      error = RCTNSErrorFromJSError(_context, jsError);
    }

    onComplete(error);
  }];
}

JSEvaluateScript 6 个参数:

arduino 复制代码
JSValueRef JSEvaluateScript(
    JSContextRef ctx,
    JSStringRef script,
    JSObjectRef thisObject,
    JSStringRef sourceURL,
    int startingLineNumber,
    JSValueRef *exception);
位置 这里传 含义
ctx _context 在哪个 JS context 里执行
script execJSString 要执行的源码
thisObject NULL 执行时的 this;NULL 表示全局对象
sourceURL sourceURL 报错堆栈里显示的 URL
startingLineNumber 0 报错堆栈里的起始行号
exception &jsError 输出参数:脚本抛异常时 JSC 把异常值写到这个指针

返回值 JSValueRef:非 NULL 表示脚本正常返回(值是 script 最后一个表达式的值;bundle 这种没顶层 return 的通常是 undefined);NULL 表示脚本抛异常,jsError 被填充。

bundle 执行的 ARC + JSC 内存管理流:

sql 复制代码
NSString *script              ← Foundation,ARC 管
  ├─ JSStringCreateWithCFString → JSStringRef execJSString  ← JSC 持有,必须 release
  ├─ JSEvaluateScript → JSValueRef result                   ← 短命,跟 GC 走,不需要 release
  │                  → JSValueRef jsError(如果错误)       ← 同上
  └─ JSStringRelease(execJSString)                          ← 释放第 1 步

核心规律:JSC API 里凡是 JSStringCreate* / JSObjectMake* 这种 Create / Make 函数都需要手动 release;凡是 JSValueRef 这种值类型都跟着 JSC 的 GC 走、不需要管。这是 CoreFoundation "Create Rule" 的延伸。

RCTNSErrorFromJSError

objectivec 复制代码
static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
{
  NSString *errorMessage = jsError ? RCTJSValueToNSString(context, jsError) : @"unknown JS error";
  NSString *details = jsError ? RCTJSValueToJSONString(context, jsError, 2) : @"no details";
  return [NSError errorWithDomain:@"JS" code:1 userInfo:@{NSLocalizedDescriptionKey: errorMessage, NSLocalizedFailureReasonErrorKey: details}];
}

errorMessage 是 JS error 对象的 String(err)(通常是 "TypeError: ...");detailsJSON.stringify(err, null, 2)(完整字段);包成 domain=@"JS" 的 NSError 往上传给 Bridge 的 enqueueApplicationScript onComplete,最终在 RCTRootView.bundleFinishedLoading: 里被 RedBox 展示。

13. executeJSCall:method:arguments:callback:

理解 bundle 跑完之后 Native 调 JS 模块方法是怎么拼字符串 + 反序列化的。

ini 复制代码
- (void)executeJSCall:(NSString *)name
               method:(NSString *)method
            arguments:(NSArray *)arguments
             callback:(RCTJavaScriptCallback)onComplete
{
  RCTAssert(onComplete != nil, @"onComplete block should not be nil");
  [self executeBlockOnJavaScriptQueue:^{
    NSError *error;
    NSString *argsString = RCTJSONStringify(arguments, &error);
    if (!argsString) {
      RCTLogError(@"Cannot convert argument to string: %@", error);
      onComplete(nil, error);
      return;
    }
    NSString *execString = [NSString stringWithFormat:@"require('%@').%@.apply(null, %@);", name, method, argsString];

    JSValueRef jsError = NULL;
    JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)execString);
    JSValueRef result = JSEvaluateScript(_context, execJSString, NULL, NULL, 0, &jsError);
    JSStringRelease(execJSString);

    if (!result) {
      onComplete(nil, RCTNSErrorFromJSError(_context, jsError));
      return;
    }

    id objcValue;
    if (!JSValueIsNull(_context, result)) {
      JSStringRef jsJSONString = JSValueCreateJSONString(_context, result, 0, nil);
      if (jsJSONString) {
        NSString *objcJSONString = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, jsJSONString);
        JSStringRelease(jsJSONString);

        objcValue = RCTJSONParse(objcJSONString, NULL);
      }
    }

    onComplete(objcValue, nil);
  }];
}

5 个阶段:

阶段 代码 干什么
序列化参数 RCTJSONStringify(arguments, &error) NSArray → JSON 字符串。失败时 RCTLogError + 早返回
拼调用字符串 [NSString stringWithFormat:@"require('%@').%@.apply(null, %@);", ...] 拼一段可执行 JS
JSEvaluateScript 同第 12 节 执行拼出来的字符串
错误转换 RCTNSErrorFromJSError(_context, jsError) result 为 NULL 时报错
反序列化返回值 JSValueCreateJSONString → JSStringCopyCFString → RCTJSONParse JS 返回值 → JSON 字符串 → ObjC 对象

**拼出来的字符串长什么样: **第 11 节 Bridge 调 executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] 拼出:

javascript 复制代码
require('BatchedBridge').flushedQueue.apply(null, []);

如果是 Bridge._invokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" arguments:@[moduleID, methodID, args]

javascript 复制代码
require('BatchedBridge').callFunctionReturnFlushedQueue.apply(null, [0,1,["arg1","arg2"]]);

这是个非常「原始」的字符串拼接式 RPC。后续 RN 版本会引入更结构化的方案,当前 commit 就这一行 stringWithFormat:

为什么是 require('%@') **而不是直接挂全局: **bundle 加载完后 JS 模块系统就绪,每个文件靠 @providesModule Xxxrequire('Xxx') 互相找。Native 想调 JS 模块,必须先 require 拿到模块对象。如果想避免 require 开销,可以让 JS 一启动就把 BatchedBridge 挂到全局(global.__BatchedBridge = require('BatchedBridge')),但 RN 当前选择「每次都 require」------require 内部有缓存,第二次以后几乎免费。简洁性 > 极致性能。

JSValueIsNull 特判优化:

ini 复制代码
if (!JSValueIsNull(_context, result)) {
  JSStringRef jsJSONString = JSValueCreateJSONString(_context, result, 0, nil);
  ...
}

注释写:"We often return null from JS when there is nothing for native side. JSONKit takes an extra hundred microseconds to handle this simple case, so we are adding a shortcut to make executeJSCall method even faster."

flushedQueue 没有排队调用时返回 null,这种情况非常常见------所以特判 null 跳过 JSON 序列化/反序列化,省下 ~100μs。null 路径上 objcValue 保持 nil,传给 onComplete 后 Bridge 的 _handleBuffer: 第一行就 if (buffer == nil || buffer == (id)kCFNull) return; 早返回------整条路径 0 开销。

**JSON 双向序列化的代价: **注释也说 "Looks like making lots of JSC API calls is slower than communicating by using a JSON string."

方案 性能
走 JSON 字符串(当前实现) 序列化 + 反序列化各 1 次,一次性大开销
用 JSC API 逐字段拷贝(JSValueToObject + JSObjectCopyPropertyNames 递归) 跨越 JSC ↔ ObjC 边界多次,反而更慢

外加注释提到的副效果:"Also it ensures that data structures don't have cycles and non-serializable fields."------JSON 不支持循环引用,序列化前会报错,避免脏数据流到 Native 侧。

第四个 sourceURL 参数的差异:

scss 复制代码
// 第 12 节(bundle):
JSEvaluateScript(_context, execJSString, NULL, sourceURL, 0, &jsError);

// 第 13 节(call):
JSEvaluateScript(_context, execJSString, NULL, NULL, 0, &jsError);

bundle 加载时传 url,RPC 调用时传 NULL。这意味着 RPC 调用如果抛异常,堆栈里看不到具体位置。但 RPC 字符串本身很短(require('X').y.apply(null, [...]) 一行),其实不需要 sourceURL。

14. 完整流程图

scss 复制代码
RCTRootView.loadBundle 调用:
  └─ [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:...]

loadBundle 已经做了一次关键准备:
  └─ [_bridge setJavaScriptExecutor:_executor]
        └─ RCTBridge.setJavaScriptExecutor:
             ├─ _javaScriptExecutor = executor;
             ├─ _latestJSExecutor = executor;  // 文件级 static
             └─ setUp
                  ├─ 实例化所有 RCTBridgeModule 子类,存入 _modulesByID / _modulesByName
                  ├─ 为每个模块 setBridge:self
                  ├─ 构造 configJSON = {remoteModuleConfig, localModulesConfig}
                  ├─ injectJSONText: configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
                  │      └─ executor.injectJSONText: ─── 投到 JS 线程 ───→
                  │           ├─ JSValueMakeFromJSONString(_context, configString)
                  │           ├─ JSContextGetGlobalObject(_context) → globalObject
                  │           ├─ JSObjectSetProperty(global, "__fbBatchedBridgeConfig", value)
                  │           └─ onComplete(nil) → dispatch_semaphore_signal
                  └─ dispatch_semaphore_wait(主线程同步阻塞最多 1 秒)

进入 enqueueApplicationScript:
  └─ RCTBridge.enqueueApplicationScript:url:onComplete:
       └─ [_javaScriptExecutor executeApplicationScript:script sourceURL:url
                              onComplete:^(NSError *scriptLoadError) {
            if (scriptLoadError) { onComplete(scriptLoadError); return; }   ← 失败早退
            [_javaScriptExecutor executeJSCall:@"BatchedBridge"                                        method:@"flushedQueue"                                     arguments:@[]
                                      callback:^(id json, NSError *error) {
              [self _handleBuffer:json];      ← 第 4 篇终点 / 第 5 篇起点
              onComplete(error);              ← 回到 RCTRootView 的 bundleFinishedLoading
            }];
          }]

RCTContextExecutor.executeApplicationScript:sourceURL:onComplete:
  └─ executeBlockOnJavaScriptQueue:^{ ─── 投到 JS 线程 ───→
       ├─ JSStringCreateWithCFString(script) → execJSString
       ├─ JSStringCreateWithCFString(url.absoluteString) → sourceURL
       ├─ JSEvaluateScript(_context, execJSString, NULL, sourceURL, 0, &jsError) → result
       │     └─ 整段 JS bundle 在 JavaScriptCore 里被执行
       │           ├─ var BatchedBridge = ... (读 __fbBatchedBridgeConfig)
       │           ├─ AppRegistry.registerComponent(...)
       │           ├─ 各种模块 require 链
       │           └─ 加载过程中 push 到 BatchedBridge 队列的 native 调用
       ├─ JSStringRelease(sourceURL) / JSStringRelease(execJSString)
       ├─ if (!result) error = RCTNSErrorFromJSError(_context, jsError);
       └─ onComplete(error)

RCTContextExecutor.executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] callback:...
  └─ executeBlockOnJavaScriptQueue:^{ ─── 同样在 JS 线程 ───→
       ├─ RCTJSONStringify(@[]) → "[]"
       ├─ execString = "require('BatchedBridge').flushedQueue.apply(null, []);"
       ├─ JSEvaluateScript(_context, execString, NULL, NULL, 0, &jsError) → result
       ├─ if (!JSValueIsNull(_context, result))
       │     ├─ JSValueCreateJSONString(_context, result, 0, nil) → jsJSONString
       │     ├─ JSStringCopyCFString → objcJSONString
       │     ├─ RCTJSONParse(objcJSONString, NULL) → objcValue (NSArray with 6 fields)
       └─ onComplete(objcValue, nil)

RCTBridge._handleBuffer:objcValue

四点收口:

  1. Bridge 在 setJavaScriptExecutor 时偷偷往 JS 全局塞了 __fbBatchedBridgeConfig
  2. RCTContextExecutor 把 JS 关在一条自建线程 + RunLoop 上跑。
  3. JSEvaluateScript 是真正执行 JS 源码的一行。
  4. bundle 跑完立刻 require('BatchedBridge').flushedQueue() 把 JS 排队的 native 调用拉回来。

一句话:Native 侧不会「理解」JS 代码。它只是把 JS 源码字符串通过 JSEvaluateScript 交给 Apple 的 JavaScriptCore.framework;同时 Bridge 在 bundle 跑之前预先注入一份模块表 JSON 到 JS 全局,bundle 跑之后通过 flushedQueue RPC 把 JS 侧排队的 native 调用取回来。

时序图

rust 复制代码
sequenceDiagram
    autonumber
    participant Root as RCTRootView
    participant Bridge as RCTBridge
    participant Exec as RCTContextExecutor
    participant JST as JS Thread<br/>(com.facebook.React.JavaScript)
    participant JSC as JavaScriptCore

    Note over Root,JSC: ━━━ 第 3 篇 setJavaScriptExecutor → 第 4 篇 setUp ━━━

    Root->>Bridge: setJavaScriptExecutor:_executor
    activate Bridge
    Bridge->>Bridge: _javaScriptExecutor = executor<br/>_latestJSExecutor = executor
    Bridge->>Bridge: setUp(同步阻塞主线程)
    Bridge->>Bridge: 实例化所有 RCTBridgeModule<br/>构造 configJSON

    Bridge->>Exec: injectJSONText: __fbBatchedBridgeConfig
    Exec->>JST: executeBlockOnJavaScriptQueue:<br/>performSelector:onThread:
    Note over JST: 切换线程
    activate JST
    JST->>JSC: JSValueMakeFromJSONString(_context, json)
    JST->>JSC: JSContextGetGlobalObject(_context)
    JST->>JSC: JSObjectSetProperty(global, "__fbBatchedBridgeConfig", value)
    JST->>Bridge: onComplete(nil)<br/>dispatch_semaphore_signal
    deactivate JST

    Note over Bridge: dispatch_semaphore_wait 返回(最长等 1s)
    deactivate Bridge

    Note over Root,JSC: ━━━ 网络下载完成,进入 enqueueApplicationScript ━━━

    Root->>Bridge: enqueueApplicationScript: rawText url: onComplete:
    activate Bridge
    Bridge->>Exec: executeApplicationScript: sourceURL: onComplete:
    Exec->>JST: executeBlockOnJavaScriptQueue:
    Note over JST: 切换到 JS 线程
    activate JST
    JST->>JSC: JSStringCreateWithCFString(script)
    JST->>JSC: JSEvaluateScript(_context, script, NULL, sourceURL, 0, &jsError)
    activate JSC
    Note over JSC: bundle 完整执行<br/>require('BatchedBridge') 读 __fbBatchedBridgeConfig<br/>各种模块 require<br/>过程中 push native 调用到 BatchedBridge 队列
    JSC-->>JST: result(成功)or jsError
    deactivate JSC
    JST->>JST: JSStringRelease × 2
    JST->>Bridge: onComplete(nil)
    deactivate JST

    Note over Bridge: 在 JS 线程上立刻发起 flushedQueue 调用
    Bridge->>Exec: executeJSCall: BatchedBridge method: flushedQueue<br/>arguments: [] callback: ...
    Exec->>JST: executeBlockOnJavaScriptQueue:<br/>(当前已在 JS 线程,同步走 else 分支)
    activate JST
    JST->>JST: 拼字符串<br/>"require('BatchedBridge').flushedQueue.apply(null, [])"
    JST->>JSC: JSEvaluateScript(_context, execString, NULL, NULL, 0, &jsError)
    activate JSC
    JSC-->>JST: result(NSArray with 6 fields,可能是 null)
    deactivate JSC
    alt result is null
        JST->>Bridge: onComplete(nil, nil)
    else result has data
        JST->>JSC: JSValueCreateJSONString(result, 0, nil)
        JST->>JST: JSStringCopyCFString → RCTJSONParse → objcValue
        JST->>Bridge: onComplete(objcValue, nil)
    end
    deactivate JST

    Bridge->>Bridge: _handleBuffer:json
    Note right of Bridge: 🚪 第 4 篇终点 / 第 5 篇入口
    Bridge->>Root: enqueueApplicationScript 的 onComplete(error)
    deactivate Bridge

    Note over Root,JSC: Root 内部 dispatch_async main_queue<br/>切回主线程跑 bundleFinishedLoading<br/>(第 3 篇任务 11 展开)
相关推荐
假如让我当三天老蒯2 小时前
State和Props区别和左右(自学用)
前端·react.js
西部荒野子2 小时前
1. 建立源码地图
前端
西部荒野子2 小时前
3.RCTRootView 加载 Bundle 流程
前端
西部荒野子2 小时前
2.iOS 启动到 RCTRootView
前端
scan7242 小时前
SystemMessage,HumanMessage,AIMessage,ToolMessage
开发语言·前端·javascript
AI_零食2 小时前
鸿蒙PC Electron跨平台应用开发:辗转相除法计算器实现详解
前端·学习·华为·electron·开源·鸿蒙·鸿蒙系统
rising start2 小时前
二、Vue3 核心基础:API 对比、Setup 与响应式详解
前端·javascript·vue.js
ofoxcoding2 小时前
MiniMax M3 实测手记:踩完坑之后,我总结了报错处理和省 token 的几个办法
java·前端·人工智能·ai
YG亲测源码屋3 小时前
html表白代码大全可复制免费 html表白网页制作源码
前端·html