RCTRootView 从拿到 bundleURL,到把 JS 根组件挂起来的全部内部流程。
整条链路一眼概览:
ini
AppDelegate.m
-> [[RCTRootView alloc] initWithBundleURL:moduleName:launchOptions:]
├─ +initialize <- 注册 Cmd-R / Cmd-D 全局快捷键
├─ 保存 _moduleName / _launchOptions
├─ setUp <- reactTag / DEBUG 开发菜单 / reload 通知监听
└─ setScriptURL: <- 关键开关,触发 loadBundle
└─ loadBundle
├─ invalidate(双重清理 touchHandler / executor / bridge)
├─ _registered = NO
├─ 创建 executor(三段回退链)
├─ 创建 RCTBridge(initWithBundlePath:moduleProvider:launchOptions:)
├─ [_bridge setJavaScriptExecutor:_executor]
├─ 创建 RCTTouchHandler 并 addGestureRecognizer
├─ NSURLSession dataTaskWithURL:_scriptURL
├─ 编码探测 → NSData 转 rawText
├─ 错误处理(NSURLErrorDomain / HTTP 状态码 / JSON 解析 / RedBox)
├─ RCTSourceCode.scriptURL / scriptText 注入
└─ [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^{
dispatch_async(main_queue, ^{
if (_bridge.isValid) {
[self bundleFinishedLoading:_error];
├─ [_bridge.uiManager registerRootView:self]
├─ _registered = YES
└─ [_bridge enqueueJSCall:@"AppRegistry.runApplication"
args:@[moduleName, appParameters]]
}
});
}]
阅读文件
| 文件 | 作用 | 关注点 |
|---|---|---|
React/Base/RCTRootView.h |
RCTRootView 对外接口 | scriptURL 的 doc-comment |
React/Base/RCTRootView.m |
RCTRootView 全部实现 | loadBundle / bundleFinishedLoading |
React/Base/RCTBridge.h |
Bridge 对外接口 | 构造方法、enqueueJSCall、enqueueApplicationScript |
React/Base/RCTBridge.m |
Bridge 实现 | --- |
React/Executors/RCTContextExecutor.m |
默认 JS executor | --- |
React/Base/RCTTouchHandler.m |
触摸事件接入 | --- |
React/Modules/RCTSourceCode.h |
暴露给 JS 的源码信息模块 | 公开属性 scriptURL / scriptText |
1. RCTRootView.m 的整体结构
先看清这个文件有多少方法、几个实例变量、哪些是 RN 自己加的协议、哪些是 UIKit 覆盖。
实例变量与静态变量:
ini
@implementation RCTRootView
{
RCTDevMenu *_devMenu;
RCTBridge *_bridge;
RCTTouchHandler *_touchHandler;
id<RCTJavaScriptExecutor> _executor;
BOOL _registered;
NSDictionary *_launchOptions;
}
static Class _globalExecutorClass;
方法清单:
| 方法 | 类型 | 何时被调 |
|---|---|---|
+initialize |
类初始化(系统自动调一次) | 类第一次被使用时 |
-initWithBundleURL:moduleName:launchOptions: |
实例初始化 | AppDelegate 创建时 |
-_initWithBundleURL:moduleName:launchOptions:moduleProvider: |
私有测试用初始化 | 仅测试 |
-setUp |
内部初始化 | init 内部 |
-canBecomeFirstResponder / -motionEnded:withEvent: |
UIKit 覆盖 | 摇一摇时 |
+JSMethods |
RN 约定:声明本类会调用的 JS 方法 | Bridge 启动时收集 |
-dealloc |
释放 | 实例销毁时 |
-isValid / -invalidate |
RCTInvalidating 协议 | reload / dealloc |
-bundleFinishedLoading: |
bundle 加载完毕的回调 | enqueueApplicationScript 完成回调 |
-loadBundle |
核心:拉 bundle、建 bridge、跑 script | setScriptURL: / reload |
-setScriptURL: |
触发器 | init 末尾 / 外部不可调(readonly) |
-layoutSubviews |
UIKit 覆盖 | 系统布局时 |
-reload / +reloadAll |
reload API | 摇一摇 / Cmd-R / 通知 |
-startOrResetInteractionTiming / -endAndResetInteractionTiming |
性能埋点 | 业务调用 |
2. +initialize:类第一次被加载时
目标:理解这个类第一次被加载到运行时时会做什么。
objectivec
+ (void)initialize
{
#if TARGET_IPHONE_SIMULATOR
// Register Cmd-R as a global refresh key
[[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"r"
modifierFlags:UIKeyModifierCommand
action:^(UIKeyCommand *command) {
[self reloadAll];
}];
// Cmd-D reloads using the web view executor, allows attaching from Safari dev tools.
[[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"d"
modifierFlags:UIKeyModifierCommand
action:^(UIKeyCommand *command) {
_globalExecutorClass = NSClassFromString(@"RCTWebSocketExecutor");
if (!_globalExecutorClass) {
RCTLogError(@"WebSocket debugger is not available. Did you forget to include RCTWebSocketExecutor?");
}
[self reloadAll];
}];
#endif
}
要点:
-
+initialize是 ObjC runtime 在类第一次被使用前自动调用的钩子,每个类最多被调一次(子类会单独调一次),你不需要主动触发它。 - 整个方法体被
#if TARGET_IPHONE_SIMULATOR包住,所以真机不会跑这段,两个快捷键只在模拟器里有效。 - Cmd-R →
[self reloadAll](类方法里self就是RCTRootView类本身)→ 发RCTReloadNotification→ 所有RCTRootView实例 reload。 - Cmd-D → 把全局
_globalExecutorClass切到RCTWebSocketExecutor,然后 reload。下一次loadBundle创建 executor 时就走 WebSocket 调试器(连到 Chrome / Safari dev tools)。 -
_globalExecutorClass = NSClassFromString(@"RCTWebSocketExecutor")用字符串反射拿类,是因为RCTWebSocketExecutor是可选模块,可能没编进二进制------反射失败就RCTLogError,但不会崩。
3. initWithBundleURL:moduleName:launchOptions:
ini
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
launchOptions:(NSDictionary *)launchOptions
{
if ((self = [super init])) {
RCTAssert(bundleURL, @"A bundleURL is required to create an RCTRootView");
RCTAssert(moduleName, @"A bundleURL is required to create an RCTRootView");
_moduleName = moduleName;
_launchOptions = launchOptions;
[self setUp];
[self setScriptURL:bundleURL];
}
return self;
}
| 行 | 做的事 | 备注 |
|---|---|---|
self = [super init] |
调 UIView 的 init | RN 没传 frame,root view 的 frame 后面由 UIViewController 给(铺满父 view) |
RCTAssert(bundleURL, ...) |
断言 bundleURL 非空 | DEBUG 下不满足会断言失败,Release 下不会 |
RCTAssert(moduleName, ...) |
断言 moduleName 非空 | 注意 RN 的 typo:错误信息写成了 "A bundleURL is required",其实应该是 "A moduleName is required"------这是当前 commit 里真实存在的 bug(见 RCTRootView.m:83),可以用它验证你读的是不是同一版本源码 |
_moduleName = moduleName |
保存 JS 根组件名 | 后面 bundleFinishedLoading: 拿去调 AppRegistry.runApplication |
_launchOptions = launchOptions |
保存启动参数 | 后面 loadBundle 透传给 RCTBridge |
[self setUp] |
RN 自己的基础初始化 | --- |
[self setScriptURL:bundleURL] |
关键:通过 setter 触发 loadBundle | --- |
setUp 放在 setScriptURL: 之前是有意为之:setUp 会创建 reactTag、注册 reload 通知;只有这些基础状态准备好,loadBundle 启动的后续异步链路才完整。当前下载 / 执行 bundle 是异步的,调换顺序不一定立刻让 registerRootView: 看到 nil,但它会让「启动加载」发生在 root view 还没初始化完的状态下,代码意图明显不如现在清楚。
4. setUp
objectivec
- (void)setUp
{
// Every root view that is created must have a unique react tag.
// Numbering of these tags goes from 1, 11, 21, 31, etc
static NSInteger rootViewTag = 1;
self.reactTag = @(rootViewTag);
#ifdef DEBUG
self.enableDevMenu = YES;
#endif
self.backgroundColor = [UIColor whiteColor];
rootViewTag += 10;
// Add reload observer
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reload)
name:RCTReloadNotification
object:nil];
}
| 行 | 做的事 | 为什么这么做 |
|---|---|---|
static NSInteger rootViewTag = 1; |
进程级计数器 | static 让它跨实例累加,每次创建 root view 拿走一个 tag |
self.reactTag = @(rootViewTag); |
把当前 tag 包成 NSNumber 存到 UIView+React 分类的属性里 |
UIManager / Bridge 用 reactTag 在 JS 和 Native 之间映射 view |
enableDevMenu = YES(DEBUG) |
打开摇一摇开发菜单 | 配合 motionEnded: 使用 |
backgroundColor = whiteColor |
默认白底 | bundle 加载期间不留黑屏 |
rootViewTag += 10 |
下一个 root view 的 tag 加 10(不是 +1) | 保证所有 root view 的 tag 都满足 tag % 10 == 1,与 JS 侧普通 view tag 分开 |
注册 RCTReloadNotification 监听 |
收到通知就 [self reload] |
让 +reloadAll 能广播到每个实例 |
reactTag 为什么是 1, 11, 21, 31...
RN 给 view 分配 reactTag 用的是「区段」策略:
bash
root view #0 -> 1
root view #1 -> 11
root view #2 -> 21
root view #3 -> 31
关键不是「每个 root 预留几个子 view 编号」,而是保留一整类数字:所有 tag % 10 == 1 的值都属于 Native root view。JS 侧分配普通 view tag 时会跳过这些 root tag,所以不会冲突。当前版本绝大多数 App 只有一个 root view,所以你通常只看到 1。
reload 通知
objectivec
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reload)
name:RCTReloadNotification
object:nil];
这个监听是 +reloadAll 能「一次重启全部 root view」的实现机制:
rust
+reloadAll
-> [NSNotificationCenter postNotificationName:RCTReloadNotification ...]
-> 每个 RCTRootView 实例都收到 reload
-> 每个实例都跑 [self loadBundle]
Cmd-R、摇一摇开发菜单的 reload 项、TestRunner 重置,最终都走这一条广播。
5. setScriptURL:
ini
- (void)setScriptURL:(NSURL *)scriptURL
{
if ([_scriptURL isEqual:scriptURL]) {
return;
}
_scriptURL = scriptURL;
[self loadBundle];
}
对外暴露的是只读属性:
objectivec
@property (nonatomic, strong, readonly) NSURL *scriptURL;
.h 声明 readonly,.m 里却能写 setScriptURL:------这是合法的:readonly 只限制对外,类内部仍可定义并调用同名 setter。它就是触发 loadBundle 的「关键开关」。
6. loadBundle 第 1 段:双重 invalidate
ini
- (void)loadBundle
{
[self invalidate];
if (!_scriptURL) {
return;
}
// Clean up
[self removeGestureRecognizer:_touchHandler];
[_touchHandler invalidate];
[_executor invalidate];
[_bridge invalidate];
_registered = NO;
...
invalidate 内部:
csharp
- (void)invalidate
{
// Clear view
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self removeGestureRecognizer:_touchHandler];
[_touchHandler invalidate];
[_executor invalidate];
// TODO: eventually we'll want to be able to share the bridge between
// multiple rootviews, in which case we'll need to move this elsewhere
[_bridge invalidate];
}
loadBundle 先调 [self invalidate],紧接着又把 touchHandler / executor / bridge 的 invalidate 重复了一遍:
| 动作 | invalidate 里 | loadBundle 紧接着又来一次 |
|---|---|---|
| 清掉所有 subview | ✅ | ❌ |
removeGestureRecognizer:_touchHandler |
✅ | ✅ |
[_touchHandler invalidate] |
✅ | ✅ |
[_executor invalidate] |
✅ | ✅ |
[_bridge invalidate] |
✅ | ✅ |
这是这版的冗余代码:除了「清 subview」只在 invalidate 里做一次,其余三件清理被做了两遍,行为上无害但确实重复。
7. loadBundle 第 2 段:executor 三段回退链
vbnet
// Choose local executor if specified, followed by global, followed by default
_executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init];
ObjC 的 ?: 是 GCC/Clang 扩展的「左侧非 nil/非 0 就用左侧,否则用右侧」。所以这一行等价于:
ini
Class chosenClass;
if (_executorClass != Nil) {
chosenClass = _executorClass;
} else if (_globalExecutorClass != Nil) {
chosenClass = _globalExecutorClass;
} else {
chosenClass = [RCTContextExecutor class];
}
_executor = [[chosenClass alloc] init];
| 优先级 | 来源 | 谁设置 |
|---|---|---|
| 1(最高) | _executorClass(实例属性) |
业务代码:rootView.executorClass = ... |
| 2 | _globalExecutorClass(静态变量) |
Cmd-D / 开发菜单 |
| 3(默认) | RCTContextExecutor(基于 JavaScriptCore) |
没人改时 |
8. loadBundle 第 3 段:RCTBridge 与 RCTTouchHandler
css
/**
* HACK(t6568049) Most of the properties passed into the bridge are not used
* right now but it'll be changed soon so it's here for convenience.
*/
_bridge = [[RCTBridge alloc] initWithBundlePath:_scriptURL.absoluteString
moduleProvider:_moduleProvider
launchOptions:_launchOptions];
[_bridge setJavaScriptExecutor:_executor];
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
[self addGestureRecognizer:_touchHandler];
| 步骤 | 代码 | 作用 |
|---|---|---|
| 创建 bridge | [[RCTBridge alloc] initWithBundlePath:moduleProvider:launchOptions:] |
bridge 创建 eventDispatcher / shadowQueue,保存 module provider、launchOptions;这一步本身不加载 bundle,只是建对象 |
| 注入 executor | [_bridge setJavaScriptExecutor:_executor] |
bridge 不自己创建 executor,由外部注入。注意这个方法在 RCTBridge.h 没有公开声明,是 root view 通过类扩展(文件顶部那段 @interface RCTBridge (RCTRootView))偷偷拿来用的 |
| 创建 touch handler | [[RCTTouchHandler alloc] initWithBridge:_bridge] + addGestureRecognizer: |
touch handler 是一个 UIGestureRecognizer,挂在 root view 上拦截触摸;事件最终通过 bridge 转给 JS |
bridge 构造方法的签名:
objectivec
- (instancetype)initWithBundlePath:(NSString *)bundlepath
moduleProvider:(RCTBridgeModuleProviderBlock)block
launchOptions:(NSDictionary *)launchOptions NS_DESIGNATED_INITIALIZER;
三个对象的持有关系:
RCTRootView
├─ 持有 _executor (独立创建)
├─ 持有 _bridge (依赖 _scriptURL / _moduleProvider / _launchOptions;执行 JS 时依赖 _executor)
└─ 持有 _touchHandler (依赖 _bridge;触摸事件最终要通过 bridge 转给 JS)
9. loadBundle 第 4 段:NSURLSession 下载 + 错误处理
objectivec
// Load the bundle
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:_scriptURL completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// Handle general request errors
if (error) {
if ([[error domain] isEqualToString:NSURLErrorDomain]) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: @"Could not connect to development server. Ensure node server is running - run 'npm start' from React root",
NSLocalizedFailureReasonErrorKey: [error localizedDescription],
NSUnderlyingErrorKey: error,
};
error = [NSError errorWithDomain:@"JSServer"
code:error.code
userInfo:userInfo];
}
[self bundleFinishedLoading:error];
return;
}
// Parse response as text
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName != nil) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
if (cfEncoding != kCFStringEncodingInvalidId) {
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
// Handle HTTP errors
if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]]) {
userInfo = @{
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
@"stack": @[@{
@"methodName": errorDetails[@"description"] ?: @"",
@"file": errorDetails[@"filename"] ?: @"",
@"lineNumber": errorDetails[@"lineNumber"] ?: @0
}]
};
} else {
userInfo = @{NSLocalizedDescriptionKey: rawText};
}
error = [NSError errorWithDomain:@"JSServer"
code:[(NSHTTPURLResponse *)response statusCode]
userInfo:userInfo];
[self bundleFinishedLoading:error];
return;
}
...
}];
[task resume];
| 顺序 | 做什么 | 边界条件 |
|---|---|---|
| 1 | dataTaskWithURL:_scriptURL 异步下载 bundle |
block 在 NSURLSession 自己的后台队列上调用,不是主线程 |
| 2 | 网络错误分支(NSURLErrorDomain) |
重新包装成 JSServer 域的 NSError,提示「did you forget to run npm start?」 |
| 3 | 编码探测:HTTP response 带 charset 就用它,否则默认 NSUTF8StringEncoding |
用 CFStringConvert... 在 IANA charset 名和 NSStringEncoding 间转换 |
| 4 | HTTP 状态码 ≠ 200 分支 | 尝试把 body 当 JSON 解,拿结构化错误信息塞 stack;解不出来就把整个 body 当错误描述 |
10. loadBundle 第 5 段:成功路径
ini
if (!_bridge.isValid) {
return; // Bridge was invalidated in the meanwhile
}
// Success!
RCTSourceCode *sourceCodeModule = _bridge.modules[NSStringFromClass([RCTSourceCode class])];
sourceCodeModule.scriptURL = _scriptURL;
sourceCodeModule.scriptText = rawText;
[_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *_error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (_bridge.isValid) {
[self bundleFinishedLoading:_error];
}
});
}];
}];
[task resume];
| 顺序 | 做什么 | 关键点 |
|---|---|---|
| 1 | if (!_bridge.isValid) return; |
下载期间用户可能触发了 reload,老 bridge 已被 invalidate;老回调直接丢弃 |
| 2 | 给 RCTSourceCode 模块写回 scriptURL / scriptText |
这是个 Native Module,bridge 注入 executor 时已实例化好放在 modules 字典里;这里把源码注入进去,JS 侧后续可以 require('RCTSourceCode') 拿到 |
| 3 | [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^...] |
真正把 JS 源码交给 bridge,再由 bridge 交给 executor 执行;完成后切回主线程调 bundleFinishedLoading: |
完整调用树
ini
[+initialize(类第一次被用时)]
├─ 模拟器:注册 Cmd-R → reloadAll
└─ 模拟器:注册 Cmd-D → 切换 _globalExecutorClass + reloadAll
AppDelegate.m
-> [[RCTRootView alloc] initWithBundleURL:moduleName:launchOptions:]
├─ RCTAssert(bundleURL) / RCTAssert(moduleName)
├─ _moduleName = moduleName
├─ _launchOptions = launchOptions
├─ setUp
│ ├─ reactTag = 1(之后 +10 递增)
│ ├─ DEBUG 下 enableDevMenu = YES
│ ├─ backgroundColor = white
│ └─ 监听 RCTReloadNotification → -reload
└─ setScriptURL:bundleURL
├─ _scriptURL = bundleURL
└─ loadBundle
├─ invalidate(清 subviews / touchHandler / executor / bridge)
├─ if (!_scriptURL) return;
├─ 冗余再清理一次 touchHandler / executor / bridge
├─ _registered = NO
├─ _executor = (_executorClass ?: _globalExecutorClass ?: RCTContextExecutor) alloc init
├─ _bridge = [RCTBridge initWithBundlePath:moduleProvider:launchOptions:]
├─ [_bridge setJavaScriptExecutor:_executor]
├─ _touchHandler = [RCTTouchHandler initWithBridge:_bridge]
├─ [self addGestureRecognizer:_touchHandler]
├─ NSURLSession dataTaskWithURL:_scriptURL
│ └─ completionHandler (后台队列):
│ ├─ error 分支:包装成 JSServer NSError → bundleFinishedLoading:
│ ├─ 编码探测 → NSData 转 rawText
│ ├─ HTTP 非 200 分支:JSON 解 stack → JSServer NSError → bundleFinishedLoading:
│ ├─ if (!_bridge.isValid) return;
│ ├─ 把 rawText / _scriptURL 注入 RCTSourceCode
│ └─ [_bridge enqueueApplicationScript:rawText url:_scriptURL
│ onComplete:^(NSError *_error){
│ dispatch_async(main, ^{
│ if (_bridge.isValid) [self bundleFinishedLoading:_error];
│ });
│ }]
└─ [task resume]
bundleFinishedLoading:(成功路径在主线程;错误 early return 路径在 URLSession 回调线程)
├─ error != nil
│ └─ RedBox showErrorMessage:withStack: / withDetails:
└─ error == nil
├─ [_bridge.uiManager registerRootView:self]
├─ _registered = YES
└─ [_bridge enqueueJSCall:@"AppRegistry.runApplication"
args:@[moduleName, @{rootTag, initialProps}]]
──▶ 第 4 篇边界:进入 Bridge / Executor 内部
时序图
rust
sequenceDiagram
autonumber
participant AppDel as AppDelegate
participant Root as RCTRootView
participant Exec as RCTContextExecutor
participant Bridge as RCTBridge
participant Touch as RCTTouchHandler
participant Session as NSURLSession
participant Source as RCTSourceCode
participant UIM as RCTUIManager
participant Red as RCTRedBox
Note over AppDel,Red: ━━━ 同步段 · 主线程 ━━━
AppDel->>Root: initWithBundleURL:moduleName:launchOptions:
Root->>Root: setUp(reactTag / DEBUG enableDevMenu / 监听 RCTReloadNotification)
Root->>Root: setScriptURL: bundleURL
Root->>Root: loadBundle
Root->>Root: invalidate(双重清理 touchHandler / executor / bridge)
rect rgb(255, 245, 200)
Note over Root,Touch: 三件套装配
Root->>Exec: alloc init<br/>(_executorClass ?: _globalExecutorClass ?: RCTContextExecutor)
Root->>Bridge: initWithBundlePath:moduleProvider:launchOptions:
Root->>Bridge: setJavaScriptExecutor:_executor(类扩展偷渡)
Root->>Touch: initWithBridge:_bridge
Root->>Root: addGestureRecognizer:_touchHandler
end
Root->>Session: dataTaskWithURL:_scriptURL
Root->>Session: [task resume]
Root-->>AppDel: 同步段结束,didFinishLaunching 返回 YES
Note over AppDel,Red: 异步分割线 · 切到 NSURLSession 后台队列
Session->>Root: completion(data, response, error)
alt error != nil(失败分支)
Root->>Root: 包装成 JSServer NSError
Root->>Root: bundleFinishedLoading: error
Root->>Red: showErrorMessage:withStack: / withDetails:
Note right of Red: 红屏,等待用户重试
else error == nil(成功分支)
Root->>Root: 编码探测 → NSData 转 rawText
Root->>Root: HTTP 非 200 校验(也可能转入失败分支)
Root->>Root: if (!_bridge.isValid) return(第 1 次防御)
Root->>Source: scriptURL = _scriptURL<br/>scriptText = rawText
Root->>Bridge: enqueueApplicationScript: url: onComplete:
Note right of Bridge: 🚪 bridge 内部
Note over AppDel,Red: 异步分割线 · 切到 bridge shadowQueue(JS thread)
Bridge->>Root: onComplete(_error)
Root->>Root: dispatch_async(main_queue, ^{...})
Note over AppDel,Red: 异步分割线 · 切回主线程
Root->>Root: if (_bridge.isValid) bundleFinishedLoading:(第 2 次防御)
Root->>UIM: registerRootView: self
Root->>Root: _registered = YES
Root->>Bridge: enqueueJSCall: AppRegistry.runApplication<br/>args: [moduleName, {rootTag, initialProps}]
Note over Bridge: Bridge 入口
end
线程切换图:
objectivec
init / loadBundle 同步段 → 主线程
NSURLSession completion → 后台队列
bridge enqueueApplicationScript → 交给 executor
onComplete → 默认 JavaScript 线程
dispatch_async(main_queue, ^{}) → 主线程
bundleFinishedLoading → 主线程(成功路径)