乐于分享
好东西不私藏

Native层启动——App是怎么被操作系统唤起的

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 AppDelegateRCTAppDelegate {    // RCTAppDelegate 是 Expo 封装的基类(Swift),内部处理了 RN 初始化逻辑    // 不需要你手动创建 RCTBridge    // ========== 应用启动完成时调用 ==========    override func application(        _ applicationUIApplication,        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKeyAny]? = nil    ) -> Bool {        // 调用父类启动方法(内部做了 Hermes 初始化、JSI 安装、Fabric 注册)        return super.application(application, didFinishLaunchingWithOptions: launchOptions)    }    // ========== JS Bundle 从哪里加载(关键!)==========    // RCTAppDelegate 的 sourceURL 方法    override func sourceURL(for bridgeRCTBridge) -> URL? {        #if DEBUG            // DEBUG 模式:从 Metro dev server 实时加载(热更新)            // bridge.bundleURL = http://localhost:8081/index.bundle            return 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 AppDelegateRCTAppDelegate(第 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(    _ applicationUIApplication,    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKeyAny]? = nil) -> Bool {    // 1. 调用父类(必须,RN 初始化在里面)    let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)    // 2. 自定义逻辑(比如配置推送)    // UIApplication.shared.registerForRemoteNotifications()    return result}// 3. 处理推送 token(如果有)override func application(    _ applicationUIApplication,    didRegisterForRemoteNotificationsWithDeviceToken deviceTokenData) {    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 文件)