2.iOS 启动到 RCTRootView

这一篇回答三个问题:谁创建了 RCTRootView、创建时传了什么参数、它最后又是怎么显示到 iOS 屏幕上的。

核心链路:

rust 复制代码
main.m
  -> UIApplicationMain
  -> AppDelegate
  -> application:didFinishLaunchingWithOptions:
  -> 创建 RCTRootView
  -> 创建 UIWindow 和 UIViewController
  -> 把 RCTRootView 挂到 UIWindow 上显示

时序总览

swift 复制代码
sequenceDiagram
    autonumber
    participant Kernel as iOS Kernel
    participant Main as main.m
    participant UIAppMain as UIApplicationMain
    participant AppDel as AppDelegate
    participant RootView as RCTRootView
    participant VC as UIViewController
    participant Win as UIWindow
    participant Screen as UIScreen

    Kernel->>Main: exec() 启动进程
    activate Main
    Main->>Main: @autoreleasepool
    Main->>UIAppMain: UIApplicationMain(argc, argv, nil, "AppDelegate")
    deactivate Main
    activate UIAppMain
    Note over UIAppMain: 启动 iOS 主 RunLoop<br/>函数不会返回,直到 App 退出

    UIAppMain->>AppDel: alloc / init(runtime 反射创建实例)
    activate AppDel
    UIAppMain->>AppDel: application:didFinishLaunchingWithOptions:

    Note over AppDel: ===== didFinishLaunching 内部 5 件事 =====

    AppDel->>AppDel: ① 准备 jsCodeLocation (NSURL)
    Note right of AppDel: OPTION 1(开发)<br/>http://localhost:8081/...<br/>TicTacToeApp.includeRequire.runModule.bundle<br/><br/>OPTION 2(生产)<br/>NSBundle main.jsbundle

    rect rgb(255, 245, 200)
    AppDel->>RootView: ② initWithBundleURL: moduleName:@"TicTacToeApp" launchOptions:
    activate RootView
    Note over RootView: bundleURL  → 内部 _scriptURL<br/>moduleName → AppRegistry.runApplication 用<br/>launchOptions → 透传给 RCTBridge<br/><br/>内部异步触发 loadBundle
    deactivate RootView
    end

    AppDel->>Screen: ③ [UIScreen mainScreen].bounds
    Screen-->>AppDel: CGRect(整屏尺寸)
    AppDel->>Win: [[UIWindow alloc] initWithFrame: bounds]
    activate Win

    AppDel->>VC: ④ [[UIViewController alloc] init]
    activate VC
    AppDel->>VC: rootViewController.view = rootView
    Note over VC,RootView: 套壳:UIWindow.rootViewController 必须是<br/>UIViewController,不能直接是 UIView

    AppDel->>Win: window.rootViewController = vc
    AppDel->>Win: ⑤ makeKeyAndVisible
    Note over Win: RN 页面显示到 iOS 屏幕

    AppDel-->>UIAppMain: return YES
    deactivate VC
    deactivate Win
    deactivate AppDel
    Note over UIAppMain: RunLoop 持续运行<br/>等待用户交互 / 系统事件

视图层级

ini 复制代码
flowchart TB
    Screen[UIScreen.mainScreen<br/>物理屏幕]
    Window[UIWindow<br/>AppDelegate.window<br/>覆盖整屏]
    VC[UIViewController<br/>空壳,只为满足 API 约束]
    Root[RCTRootView : UIView RCTInvalidating<br/>RN 在 iOS 上的总入口]

    Screen -.覆盖.-> Window
    Window -- rootViewController --> VC
    VC -- view --> Root

    subgraph Params[构造时传入的 3 个参数]
        P1[bundleURL]
        P2[moduleName: TicTacToeApp]
        P3[launchOptions]
    end

    subgraph Props[RCTRootView 对外暴露]
        A1[scriptURL  readonly]
        A2[moduleName  readonly]
        A3[moduleProvider  readonly]
        A4[initialProperties]
        A5[executorClass]
        A6[enableDevMenu]
        A7[reload / reloadAll]
    end

    P1 ==> A1
    P2 ==> A2
    P3 -. 透传给 RCTBridge .-> Root
    Root --- Props

    style Root fill:#fff5c8,stroke:#d4a017,stroke-width:2px
    style P1 fill:#cfe2ff
    style P2 fill:#cfe2ff
    style P3 fill:#cfe2ff

moduleName 的三方一致

ini 复制代码
flowchart TB
    PM["@providesModule TicTacToeApp<br/>(在 TicTacToeApp.js 文件头注释)"]
    JS["AppRegistry.registerComponent('TicTacToeApp', () => TicTacToeApp)<br/>(JS 运行时根组件注册)"]
    Native["RCTRootView initWithBundleURL:...<br/>moduleName:@&quot;TicTacToeApp&quot;<br/>(Native 侧传入)"]

    PM -. packager / Haste<br/>解析 require .-> JS
    JS == 必须字符串完全相等 ==> Native
    Native == 必须字符串完全相等 ==> JS

    style JS fill:#d4edda
    style Native fill:#cfe2ff
    style PM fill:#e2e3e5

涉及文件

文件 作用
Examples/TicTacToe/TicTacToe/main.m iOS App 进程入口
Examples/TicTacToe/TicTacToe/AppDelegate.h 声明 App 生命周期代理和窗口
Examples/TicTacToe/TicTacToe/AppDelegate.m 创建 RCTRootView,把 RN 页面接入 iOS
React/Base/RCTRootView.h RCTRootView 对外暴露的初始化方法和属性
Examples/TicTacToe/TicTacToeApp.js JS 侧入口,验证 moduleName 的对应

1. 入口文件 main.m

main.m 是 iOS App 的入口文件:

objectivec 复制代码
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
  @autoreleasepool {
      return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

真正启动 App 的是这一行:

objectivec 复制代码
UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

UIApplicationMain 的四个参数:

参数 这里传什么 含义
argc argc C main 函数的参数个数
argv argv C main 函数的参数数组
principalClassName nil UIApplication 子类名;nil 表示用默认的 UIApplication
delegateClassName NSStringFromClass([AppDelegate class]) App 生命周期代理类名,告诉 iOS 用 AppDelegate 处理生命周期回调

几个要点:

  • main.m 是 iOS App 的进程入口(C 函数入口),但不是 RN 的入口。
  • UIApplicationMain 启动 iOS 应用主 RunLoop,并由 iOS 自己实例化 AppDelegate
  • 第 4 个参数传的是类名字符串而不是类对象,因为 UIApplicationMain 内部要用 runtime 反射创建实例。
  • 一旦 UIApplicationMain 跑起来,函数不会返回,直到 App 退出。

2. AppDelegate.h:声明应用代理类

头文件只负责对外声明:这个类叫什么、继承谁、遵守什么协议、有哪些公开属性。

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

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;

@end

三个细节:

元素 作用
继承 UIResponder 让 AppDelegate 加入 iOS 响应链(responder chain),可以兜底处理未被消费的事件。几乎所有 AppDelegate 都这样写。
遵守 <UIApplicationDelegate> 接收 application:didFinishLaunchingWithOptions: 等生命周期回调。
window 属性 iOS App 的主窗口。这里手写创建 UIWindow 并用 strong 持有;命名为 window 是模板里的惯用约定,便于系统或模板按约定访问。

3. AppDelegate.m:实现启动逻辑

目标:找到 RN 接入 iOS 启动流程的关键位置,并理解 OPTION 1 / OPTION 2 两种 bundle 加载方式。

ini 复制代码
#import "AppDelegate.h"
#import "RCTRootView.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation;

  jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle"];

  // OPTION 2:从预打包的 main.jsbundle 加载
  // 先用 curl 打成静态 bundle:
  //   curl http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle -o main.jsbundle
  // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" ...];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                     moduleName:@"TicTacToeApp"
                                                  launchOptions:launchOptions];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [[UIViewController alloc] init];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

@end

这个方法是 App 启动完成后的系统回调,也是 RN 接入 iOS 启动流程的唯一连接点。它一共只做 5 件事:

  1. 准备 jsCodeLocation(OPTION 1 或 OPTION 2)
  2. 创建 RCTRootView,传入 bundleURL / moduleName / launchOptions
  3. 创建 UIWindow
  4. 创建 UIViewController,把 RCTRootView 设为它的 view
  5. viewController 设为 window.rootViewController,并 makeKeyAndVisible

launchOptions 是系统传进来的字典,包含 App 启动原因(推送通知、URL Scheme 等)。它先被透传给 RCTRootView,再交给 RCTBridge 保存;后续 JS 能不能读到,取决于具体 Native Module 是否从 bridge.launchOptions 导出,例如 LinkingIOS 会导出 initialURL

OPTION 1 与 OPTION 2 的区别:

维度 OPTION 1(开发) OPTION 2(生产)
URL 类型 http://localhost:8081/... file://...main.jsbundle
bundle 来源 packager 实时编译 预打包静态文件
需要本机服务 是(npm start 启动 packager)
热重载 支持 不支持
真机调试 要把 localhost 改成局域网 IP 无影响

4. JS bundle 地址

localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle 这个 URL 看着怪,拆开就清楚了:

arduino 复制代码
http://localhost:8081 / Examples/TicTacToe/TicTacToeApp . includeRequire . runModule . bundle
└─ 协议 + 端口 ────┘   └─ 模块路径(去掉 .js) ──────┘   └─ 选项 ──────────────┘   └─ 后缀
含义 由谁解析
http://localhost:8081 packager dev server 默认监听 8081 端口 iOS NSURL
Examples/TicTacToe/TicTacToeApp 入口 JS 模块的路径(相对仓库根),packager 据此找到 TicTacToeApp.js packager
.includeRequire 早期兼容写法里的 dotted 段;当前解析逻辑会把它从入口文件名里剔除 packager(packager/react-packager/src/Server/index.js
.runModule 同样是兼容段,会被剔除;真正的 runModule 行为当前默认开启,也可由 query string 控制 packager
.bundle 文件扩展名,packager 据此分发到 bundle handler;换成 .map 就返回 source map packager

证据在 packager/react-packager/src/Server/index.js 第 254 行附近:它把文件名里以 . 分隔的 includeRequire / runModule / bundle / map 段过滤掉,再拼回入口文件名。换句话说,packager 把这串 URL 同时当成「入口路径」和「兼容标记容器」------这是早期 RN 一个有意思的设计:

ini 复制代码
旧形态: .includeRequire.runModule.bundle
新形态: .bundle?runModule=true

5. 创建 RCTRootView

ini 复制代码
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                   moduleName:@"TicTacToeApp"
                                                launchOptions:launchOptions];

这是 Objective-C 常见的对象创建方式 [[类名 alloc] init...]alloc 分配内存,initWithBundleURL:moduleName:launchOptions: 完成初始化。

创建时传入三个关键参数:

  • jsCodeLocation:JS bundle 的地址。
  • @"TicTacToeApp":RN 模块名,要和 JS 里注册的模块名对应。
  • launchOptions:App 启动参数,继续传给 RN。

这一步得到的 rootView 就是 RN 在 iOS 原生侧的根视图。后续 JS 渲染出来的 UI,最终都显示在它里面。它不只是一个 View,而是一组入口的集合:

rust 复制代码
UIView                  -> 能挂到 UIWindow 显示
+ Bundle 加载入口        -> bundleURL / scriptURL
+ JS 模块名映射          -> moduleName
+ Bridge 配置入口        -> moduleProvider / executorClass
+ 业务初始 props 入口    -> initialProperties
+ 调试入口               -> enableDevMenu / reload
+ 失效协议               -> RCTInvalidating

6. 创建 UIWindow 和 UIViewController

ini 复制代码
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [[UIViewController alloc] init];
rootViewController.view = rootView;

这几行做三件事:创建一个和屏幕一样大的 UIWindow、创建一个普通的 UIViewController、把这个 controller 的 view 设为刚才的 RCTRootView

其中 UIViewController *rootViewController 声明的是一个指向 UIViewController 对象的指针------ObjC 里对象变量都是指针,所以类型后面带 *。类比 JS:

ini 复制代码
const rootViewController = new UIViewController();

7. 把页面显示到屏幕上

ini 复制代码
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
  • self.window.rootViewController = rootViewController;:把根控制器挂到窗口上。
  • [self.window makeKeyAndVisible];:让窗口成为主窗口并显示。
  • return YES;:告诉系统 App 启动成功。

最终的 UI 层级:

objectivec 复制代码
UIScreen.mainScreen
  └─ UIWindow                          <- AppDelegate.window
       └─ rootViewController           <- 一个空的 UIViewController
            └─ view = RCTRootView      <- RN 页面(UIView 子类)

四步对照:

步骤 代码 作用
1 [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds] 创建覆盖整屏的窗口
2 [[UIViewController alloc] init] 创建一个空 ViewController(只是个壳)
3 rootViewController.view = rootView 把 RCTRootView 挂成它的根 view
4 [self.window makeKeyAndVisible] 让窗口可见 + 成为 key window(接收事件)

8. 最终调用链

main.m 到页面显示,整条链路是:

rust 复制代码
main.m
  -> UIApplicationMain(..., @"AppDelegate")
  -> 系统创建 AppDelegate 对象
  -> 系统把它设置成 UIApplication 的 delegate
  -> App 启动完成
  -> 系统回调 application:didFinishLaunchingWithOptions:
  -> 设置 JS bundle 地址
  -> 创建 RCTRootView
  -> 创建 UIWindow
  -> 创建 UIViewController
  -> 把 RCTRootView 设置成 rootViewController.view
  -> 把 rootViewController 设置成 window.rootViewController
  -> makeKeyAndVisible
  -> 页面显示到 iOS 屏幕上
相关推荐
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
JAVA学习通3 小时前
从 Bean 到微服务:一张图吃透 Spring 全家桶底层原理
java·前端·spring
古韵3 小时前
前端请求库的下一个进化方向:从 Promise 到策略化
前端