这一篇回答三个问题:谁创建了 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:@"TicTacToeApp"<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 件事:
- 准备
jsCodeLocation(OPTION 1 或 OPTION 2) - 创建
RCTRootView,传入bundleURL/moduleName/launchOptions - 创建
UIWindow - 创建
UIViewController,把RCTRootView设为它的view - 把
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 屏幕上