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」的逻辑集中在 executeApplicationScript 和 executeJSCall 两个方法,剩下全是线程、生命周期、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 值,挂到全局对象上某个名字下 |
三个反常识细节:
- 协议只要求 3 个方法,不要求
setUp**/setBridge:****。 **这意味着 Bridge 不能假设 executor 知道自己------所有「模块表注入」都得通过injectJSONText:这条窄通道做。后面会看到__fbBatchedBridgeConfig就是这么塞进去的。 -
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。 - 返回值是
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 的「跨线程方法调用」原生机制):
- 把「调用方法 + 参数」打包成一个
NSPortMessage; - enqueue 到目标线程 RunLoop 的 input source;
- 触发 source signal,让目标线程 RunLoop 醒来;
- 目标线程 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 的形状
RCTRemoteModulesConfig 和 RCTLocalModulesConfig(RCTBridge.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: ...");details 是 JSON.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 Xxx 和 require('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
四点收口:
- Bridge 在
setJavaScriptExecutor时偷偷往 JS 全局塞了__fbBatchedBridgeConfig。 -
RCTContextExecutor把 JS 关在一条自建线程 + RunLoop 上跑。 -
JSEvaluateScript是真正执行 JS 源码的一行。 - 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 展开)