Native层启动——App是怎么被操作系统唤起的
学完本章后,你能说出:
-
iOS App 的启动入口在哪里 -
Android App 的启动入口在哪里 -
React Native 根容器(RCTRootView / ReactRootView)是如何创建的 -
为什么 appKey = "main"这个细节很重要
全局流程一览
用户点击图标│├── iOS(@main + Swift AppDelegate)│ @main宏 → UIApplication → AppDelegate → RCTRootView│└── Android(Kotlin)MainActivity.kt → ReactActivityDelegate → ReactNativeHost → ReactRootView两个平台共同完成:├── 传入 appKey = "main"├── 创建 RN 根容器视图└── 准备好 Bundle 加载器
iOS:AppDelegate 详解
核心代码
// AppDelegate.swift(Expo SDK 53+ 生成,Swift 版本)// 文件路径: ios/YourApp/AppDelegate.swiftimport UIKitimport Reactimport React_RCTAppDelegate@main// @main = Swift 入口宏,等价于 Objective-C 的 main.m 自动调用 UIApplicationMain// 语法:自动生成 main() 函数,无需手动写 main.mclass AppDelegate: RCTAppDelegate {// RCTAppDelegate 是 Expo 封装的基类(Swift),内部处理了 RN 初始化逻辑// 不需要你手动创建 RCTBridge// ========== 应用启动完成时调用 ==========override func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {// 调用父类启动方法(内部做了 Hermes 初始化、JSI 安装、Fabric 注册)return super.application(application, didFinishLaunchingWithOptions: launchOptions)}// ========== JS Bundle 从哪里加载(关键!)==========// RCTAppDelegate 的 sourceURL 方法override func sourceURL(for bridge: RCTBridge) -> URL? {#if DEBUG// DEBUG 模式:从 Metro dev server 实时加载(热更新)// bridge.bundleURL = http://localhost:8081/index.bundlereturn RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")#else// RELEASE 模式:从本地 main.jsbundle 加载(Hermes Bytecode)// Bundle 内嵌在 .app 的 main.jsbundle 中return Bundle.main.url(forResource: "main", withExtension: "jsbundle")#endif}// ========== 可选:自定义 App Key(默认就是 "main")==========// RCTAppDelegate 默认的 moduleName = "main"// 必须和 JS 层的 AppRegistry.registerComponent('main', ...) 完全匹配override var moduleName: String {return "main" // 通常不需要改}}
逐行解释
@main(第 1 行)→ Swift 5+ 入口宏,自动生成 main() 函数→ 等价于旧的 main.m 中的 UIApplicationMain()→ 不需要手动写 main() 函数了class AppDelegate: RCTAppDelegate(第 7 行)→ 继承 Expo 封装的 Swift AppDelegate 基类→ 基类内部处理 Hermes 初始化、JSI 安装、TurboModule 注册→ 比旧的 @interface/@implementation 更简洁sourceURL(for:)(第 20 行)→ 返回 JS Bundle 的来源→ DEBUG → Metro dev server(需要电脑在同一个网络)→ RELEASE → 本地 main.jsbundle(打包进 .app)→ 和旧版 Objective-C 的 sourceURLForBridge: 功能完全相同→ 只是语法从 #if DEBUG + RCTBundleURLProvider 变成了 Swift #if + RCTBundleURLProvider⚠️ moduleName = "main"(第 37 行)→ 必须和 JS 层的 AppRegistry.registerComponent('main', ...) 匹配→ 不匹配会导致:runApplication 找不到根组件 → 崩溃⚠️ main.m 已不存在!→ Swift 项目中,@main 宏自动处理入口,无需 main.m→ 如果你的项目里有 main.m,说明是旧版 Objective-C 项目(Expo SDK < 53)
iOS App 的物理结构
YourApp.app/├── Info.plist ← bundle ID、启动配置├── main.jsbundle ← JS Bundle(RELEASE 模式,Hermes Bytecode,内嵌)├── Assets.xcassets/ ← 图片资源├── YourApp(可执行文件) ← Mach-O 主程序└── Frameworks/ ← 动态库(Hermes.framework 等)
main.m 是否还需要了解?
对于 Expo SDK 53+ 项目,main.m 已经被 @main 替代,但为了理解 iOS 启动原理,仍然值得一读:
// 这是旧版 iOS 项目的 main.m(了解即可,Expo 新项目不需要)// main.m(Expo SDK < 53 或纯 RN 旧项目)#import <UIKit/UIKit.h>#import "AppDelegate.h"int main(int argc, char * argv[]) {// UIApplicationMain 创建 UIApplication 和 AppDelegate// 然后调用 AppDelegate.application(_:didFinishLaunchingWithOptions:)NSString * appDelegateClassName;@autoreleasepool {appDelegateClassName = NSStringFromClass([AppDelegate class]);}return UIApplicationMain(argc, argv, nil, appDelegateClassName);}
main.m 执行流程:main() → UIApplicationMain() → AppDelegate 实例化→ application(_:didFinishLaunchingWithOptions:)→ Hermes 初始化 → Bundle 加载 → RN 渲染但从 Expo SDK 53 起,这个文件被 @main 宏自动生成了,你不需要手动维护它。
Swift AppDelegate 中的关键配置
// 如果你需要自定义配置(比如添加推送、处理 URL 打开等)override func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {// 1. 调用父类(必须,RN 初始化在里面)let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)// 2. 自定义逻辑(比如配置推送)// UIApplication.shared.registerForRemoteNotifications()return result}// 3. 处理推送 token(如果有)override func application(_ application: UIApplication,didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)}
Android:MainActivity + ReactNativeHost
MainActivity
// 文件路径: android/app/src/main/java/com/yourapp/MainActivity.ktclass MainActivity : ReactActivity() {// ========== 关键:返回 appKey = "main" ==========// 必须和 JS AppRegistry.registerComponent('main', ...) 完全一致override fungetMainComponentName(): String = "main"// ========== 创建 ReactActivityDelegate ==========override funcreateReactActivityDelegate():ReactActivityDelegate {return DefaultReactActivityDelegate(this,mainComponentName, // 传入 "main"DefaultNewArchitectureEntryPoint.load() // 新架构:Fabric + TurboModules)}}
MainApplication:ReactNativeHost 初始化
// 文件路径: android/app/src/main/java/com/yourapp/MainApplication.ktclass MainApplication :Application(),ReactApplication {private val reactNativeHost: ReactNativeHost =object : ReactNativeHost(this) {// ========== JS 入口点 ==========// Expo 项目默认指向 expo-router/entry// 非 Expo 项目通常是 "index.js"override fungetJSMainModuleName(): String ="node_modules/expo-router/entry"// ========== 启用新架构 ==========override funnewArchEnabled(): Boolean = true// ========== 返回所有原生模块包 ==========// 这是 Android 端注册原生模块的地方override fungetPackages(): List<ReactPackage> {return PackageList(this).getPackages()// 包含:// - MainReactPackage(RN 核心)// - ExpoModulesPackage(Expo SDK)}}override fungetReactNativeHost(): ReactNativeHost = reactNativeHost}
iOS vs Android 对比
┌────────────────┬──────────────────────────┬────────────────────────┐│ 对比项 │ iOS(Swift) │ Android(Kotlin) │├────────────────┼──────────────────────────┼────────────────────────┤│ 入口方法 │ AppDelegate.swift │ MainActivity.kt ││ │ .application( │ .getMainComponentName()││ │ didFinishLaunching...) │ │├────────────────┼──────────────────────────┼────────────────────────┤│ 入口宏 │ @main(自动生成 main) │ N/A │├────────────────┼──────────────────────────┼────────────────────────┤│ RN 根视图 │ RCTRootView │ ReactRootView │├────────────────┼──────────────────────────┼────────────────────────┤│ JS 入口配置 │ sourceURL(for:) │ getJSMainModuleName() ││ │ (Bundle URL) │ ("node_modules/...") │├────────────────┼──────────────────────────┼────────────────────────┤│ JS 根组件标识 │ moduleName = "main" │ "main" ││ │ (RCTAppDelegate) │ (getMainComponentName) │├────────────────┼──────────────────────────┼────────────────────────┤│ Bundle 路径 │ main.jsbundle │ assets/index.android. ││ │ (内嵌在 .app) │ bundle │├────────────────┼──────────────────────────┼────────────────────────┤│ 原生模块包 │ Swift Package / │ ReactPackage ││ │ CocoaPods │ (Gradle) │└────────────────┴──────────────────────────┴────────────────────────┘
核心理解:appKey 是什么?
appKey 是 JS 根组件在 Native 层的”名字”,用来让 Native 找到该渲染哪个 JS 组件:
Native 侧:iOS: moduleName = "main"Android: getMainComponentName() = "main"JS 侧:AppRegistry.registerComponent('main', RootComponent)↑必须完全匹配!不匹配的后果:Native 调用 runApplication("main", ...)JS 注册表中查找 "main" → 找不到→ 崩溃:"App with appKey "main" is not registered"
验证
# 验证 1:生成原生项目,查看结构npx expo prebuild --platform ios --cleanopen ios/YourApp.xcworkspace# 验证 2:在 iOS 中找 AppDelegate(应该是 .swift,不是 .mm)find ios -name "AppDelegate.swift" | xargs head -n 30# 确认:找到的是 .swift 文件,说明是 Swift 版# 验证 3:在 Android 中找 MainActivity(Kotlin)find android -name "MainActivity.kt" | xargs cat# 验证 4:确认 appKey 匹配grep -n "moduleName" ios/YourApp/AppDelegate.swift# 预期输出包含 "main"grep -n "main" android/app/src/main/java/ # 找 getMainComponentName# 预期输出包含 getMainComponentName() = "main"# 验证 5:找到 Bundle 文件# iOSls ios/YourApp/main.jsbundle# Androidls android/app/src/main/assets/index.android.bundle# 验证 6:确认没有旧的 ObjC AppDelegatefind ios -name "AppDelegate.mm"# 预期:无输出(新版项目没有 .mm 文件)
夜雨聆风