iOS App 冷启动过程详解
冷启动(Cold Launch)是指 App 进程不在系统内存中,需要从零开始创建进程、加载可执行文件、链接动态库并初始化运行环境,直至用户可与 App 交互的完整过程。本文将从用户点击图标开始,详细描述 iOS 冷启动的每个阶段,并明确标识“加载所有镜像”的具体位置与步骤。
一、用户交互与内核响应
用户点击 SpringBoard 上的 App 图标
SpringBoard 检测触摸事件并识别目标 App。系统内核(XNU)准备
- SpringBoard 通过
posix_spawn()系统调用请求内核创建新进程。 - 内核分配虚拟内存空间、进程 ID(PID),建立初始内核结构。
- 加载器(
execve路径)开始解析 Mach-O 可执行文件格式。
- SpringBoard 通过
二、进程映像加载与动态链接器(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 | int main(int argc, char * argv[]) { |
UIApplicationMain创建UIApplication单例、设置AppDelegate,并启动主 RunLoop。
3.2 关键内部步骤
- 初始化 UIKit 基础设施(屏幕缩放因子、文本系统等)。
- 调用
[delegate application:didFinishLaunchingWithOptions:]。 - 设置主窗口
UIWindow及rootViewController(来自 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 | DYLD_PRINT_STATISTICS = 1 |
输出示例:
1 | total pre-main time: 220.51 ms (100.0%) |
其中 dylib loading time 对应“递归加载依赖 dylib”部分,rebase/binding time 对应步骤 2.2.5,两者合起来就是加载所有镜像的主要耗时。
7.2 Instruments 与 MetricKit
- Instruments 的
App Launch模板精确测量总启动时间。 - MetricKit 收集线上用户的启动数据。
八、结语
iOS 冷启动是一个从内核到用户态、从 dyld 到 UIKit 的复杂流程。“加载所有镜像”明确发生在 dyld 阶段、main() 函数之前,具体包括:解析主可执行文件、递归加载所有依赖动态库、利用共享缓存映射系统库,并对每个镜像执行 rebase 和 binding。理解这一步骤的输入与输出,是优化启动时间的基础。通过减少动态库数量、优化 +load、延迟初始化等手段,能有效缩短冷启动时间,提升用户体验。
参考:Apple 动态链接器文档、dyld 开源代码、WWDC 启动优化系列 Session