3.RCTRootView 加载 Bundle 流程

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 对外接口 构造方法、enqueueJSCallenqueueApplicationScript
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            → 主线程(成功路径)
相关推荐
西部荒野子2 小时前
1. 建立源码地图
前端
西部荒野子2 小时前
2.iOS 启动到 RCTRootView
前端
scan7242 小时前
SystemMessage,HumanMessage,AIMessage,ToolMessage
开发语言·前端·javascript
AI_零食2 小时前
鸿蒙PC Electron跨平台应用开发:辗转相除法计算器实现详解
前端·学习·华为·electron·开源·鸿蒙·鸿蒙系统
rising start2 小时前
二、Vue3 核心基础:API 对比、Setup 与响应式详解
前端·javascript·vue.js
ofoxcoding3 小时前
MiniMax M3 实测手记:踩完坑之后,我总结了报错处理和省 token 的几个办法
java·前端·人工智能·ai
YG亲测源码屋3 小时前
html表白代码大全可复制免费 html表白网页制作源码
前端·html
夜雪闻竹3 小时前
React Query + REST API 最佳实践
前端·react.js·前端框架
段ヤシ.3 小时前
回顾Java知识点,面试题汇总Day12:tomcat、 Java Web(持续更新)
java·前端·tomcat·java web