将来的自己,会感谢现在努力的自己!

0%

App冷启动过程

iOS App 冷启动过程详解

冷启动(Cold Launch)是指 App 进程不在系统内存中,需要从零开始创建进程、加载可执行文件、链接动态库并初始化运行环境,直至用户可与 App 交互的完整过程。本文将从用户点击图标开始,详细描述 iOS 冷启动的每个阶段,并明确标识“加载所有镜像”的具体位置与步骤


一、用户交互与内核响应

  1. 用户点击 SpringBoard 上的 App 图标
    SpringBoard 检测触摸事件并识别目标 App。

  2. 系统内核(XNU)准备

    • SpringBoard 通过 posix_spawn() 系统调用请求内核创建新进程。
    • 内核分配虚拟内存空间、进程 ID(PID),建立初始内核结构。
    • 加载器(execve 路径)开始解析 Mach-O 可执行文件格式。

二、进程映像加载与动态链接器(dyld)—— 加载所有镜像的核心步骤

2.1 exec() 与 dyld 接管

  • 内核将 App 的可执行文件(Mach-O)映射到进程地址空间,但不负责链接外部动态库。
  • 内核在 Mach-O 的 LC_LOAD_DYLINKER 命令指定的路径中找到动态链接器(/usr/lib/dyld),并将控制权交给 dyld。
  • dyld 是用户态的第一个代码,负责加载所有镜像并完成符号绑定。

2.2 dyld 启动与“加载所有镜像”的详细分解

dyld 执行以下步骤(全部发生在 main() 之前):

2.2.1 自身初始化

设置错误处理、内存分配器等。

2.2.2 解析主可执行文件(第一个镜像)

  • 读取 Mach-O 头部,遍历加载命令(Load Commands)。
  • 收集所有 LC_LOAD_DYLIB(显式依赖)、LC_LOAD_WEAK_DYLIB(弱链接)等命令。

2.2.3 递归加载所有依赖的动态库(生成镜像列表)

  • 根据上一步收集的依赖,递归地打开并映射每个 .dylib.framework 文件到进程地址空间。
  • 每个被加载的 Mach-O 文件(包括主可执行文件和每个动态库)称为一个镜像(Image)
  • 这个过程会持续直到所有直接和间接依赖项都被加载。

2.2.4 处理 dyld shared cache(系统库的特殊加载)

  • iOS 的大部分系统框架(如 UIKit、Foundation)已被预先链接到一个大文件中(dyld shared cache)。
  • dyld 检查共享缓存是否已映射到当前进程;若未映射则将其映射到共享地址空间。
  • 注意:从概念上讲,这也是“加载镜像”的一部分——只是这些镜像已经预先打包。

2.2.5 对所有已加载的镜像执行 fixups(地址修正与符号绑定)

  • Rebase:修正内部指针偏移量,因为 ASLR(地址空间布局随机化)导致镜像加载地址与编译时预设地址不同。
  • Binding:将符号引用(如调用 printf)指向真实的函数地址。对 lazy binding 的符号不在此立即绑定(__DATA,__la_symbol_ptr 段保留桩代码)。
  • 处理弱绑定、延迟绑定等。

“加载所有镜像”明确定位在步骤 2.2.2 → 2.2.5
即:解析主可执行文件 → 递归加载依赖 dylib → 利用共享缓存(系统库) → 对所有镜像执行 rebase/binding。

2.3 Objective-C Runtime 初始化

  • 当 dyld 加载包含 Objective-C 类的镜像时,触发 runtime 环境准备:
    • 注册类、协议、分类(Category)。
    • 初始化 read_image 回调:调用 runtime 内部的 _objc_init,为每个镜像执行 map_images
    • 处理 __objc_classlist__objc_catlist 等 section,建立全局类表。

2.4 +load 方法与静态初始化器

  • dyld 在完成所有镜像的加载和 fixups 后,遍历所有实现了 +load 方法的类和分类,按依赖顺序调用 +load
  • C++ 静态全局对象构造函数(__attribute__((constructor)))也在此阶段执行。
  • 注意+load 执行完毕后,dyld 阶段结束,控制权转给可执行文件的入口点 main()

三、进入 main() 函数与 UIKit 初始化

3.1 入口点:main()

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
  • UIApplicationMain 创建 UIApplication 单例、设置 AppDelegate,并启动主 RunLoop。

3.2 关键内部步骤

  • 初始化 UIKit 基础设施(屏幕缩放因子、文本系统等)。
  • 调用 [delegate application:didFinishLaunchingWithOptions:]
  • 设置主窗口 UIWindowrootViewController(来自 Storyboard 或代码)。
  • 窗口 makeKeyAndVisible,触发首帧布局与渲染。
  • 调用 [delegate applicationDidBecomeActive:](在 RunLoop 第一个周期中)。
  • 进入 CFRunLoopRun(),开始处理事件。

四、RunLoop 与首帧渲染

  • 主 RunLoop 启动后,Core Animation 提交第一帧到 Render Server,完成首帧显示。
  • 系统发送 UIApplicationDidBecomeActiveNotification,App 进入可交互状态。

五、冷启动时间线总结(含加载所有镜像的位置)

阶段 主要任务 备注
1. 用户点击图标 → 内核响应 fork/exec 创建进程,映射可执行文件
2. dyld 加载所有镜像 解析主可执行、递归加载依赖 dylib、处理共享缓存、rebase+binding 发生在 main() 之前
3. Runtime 初始化 + +load 注册 ObjC 类、调用 +load 方法 仍在 main() 之前
4. main()UIApplicationMain 进入 App 代码,初始化 UIKit
5. didFinishLaunching 业务初始化
6. 首帧布局与渲染 显示第一个界面
7. applicationDidBecomeActive App 可响应事件

六、启动优化关键技术点

  • 减少 +load 方法:改用 +initialize 或懒加载。
  • 减少动态库数量:合并自定义 dylib 或改用静态库。
  • 二进制重排:使用 order_file 优化启动时内存页 fault。
  • 延迟非必要任务:不要在 didFinishLaunching 中执行耗时操作。
  • 预热(Prewarming):iOS 15+ 系统可能提前启动进程,需正确处理。

七、验证与检测方法

7.1 使用 dyld 统计信息

设置环境变量:

1
2
DYLD_PRINT_STATISTICS = 1
DYLD_PRINT_STATISTICS_DETAILS = 1

输出示例:

1
2
3
4
5
total pre-main time: 220.51 ms (100.0%)
dylib loading time: 45.25 ms (20.5%)
rebase/binding time: 112.37 ms (51.0%)
ObjC setup time: 18.88 ms ( 8.5%)
initializer time: 44.01 ms (20.0%)

其中 dylib loading time 对应“递归加载依赖 dylib”部分,rebase/binding time 对应步骤 2.2.5,两者合起来就是加载所有镜像的主要耗时。

7.2 Instruments 与 MetricKit

  • Instruments 的 App Launch 模板精确测量总启动时间。
  • MetricKit 收集线上用户的启动数据。

八、结语

iOS 冷启动是一个从内核到用户态、从 dyldUIKit 的复杂流程。“加载所有镜像”明确发生在 dyld 阶段、main() 函数之前,具体包括:解析主可执行文件、递归加载所有依赖动态库、利用共享缓存映射系统库,并对每个镜像执行 rebase 和 binding。理解这一步骤的输入与输出,是优化启动时间的基础。通过减少动态库数量、优化 +load、延迟初始化等手段,能有效缩短冷启动时间,提升用户体验。


参考:Apple 动态链接器文档、dyld 开源代码、WWDC 启动优化系列 Session