启动优化
启动类型
- Cold Launch冷启动:App进程不存在,从未打开过或者被系统。用户杀死,系统需要从磁盘加载二进制文件,创建新进程。
- Warm Launch温启动:App进程还在内存中,但被挂起,部分数据可能被回收,不需要重新执行main之前的加载,但需要重新恢复状态。
- Hot Launch热启动:App在后台运行,只是被切换到前台,几乎不要做任何初始化工作
先介绍一些概念:
Mach-O
Mach-O(Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码的文件格式。
App 编译生成的二进制可执行文件就是Mach-O 格式,iOS 工程所有的类编译后会生成对应的目标文件.o文件,而这个可执行文件就是这些.o文件的集合
- Header头部
- 标识这是一个 Mach-O 文件
- CPU架构
- 文件类型:是可执行文件(Executable)、动态库(Dylib)还是目标文件(Object)
- Load Commands加载命令
告诉操作系统如何加载和处理这个文件- 加载段(LC_SEGMENT)
- 需要加载的动态库(LC_LOAD_DYLIB)
- 程序的入口(LC_MAIN)
- 加密信息:标示文件是否被加密
- Data数据区
这部分存放了实际的代码和数据,被划分为多个 Segment(段),每个Segment 下包含多个 Section(节)。- __TEXT(代码段):只读可执行 r-x,存放被执行的代码和只读常量
- __DATA(数据段):可读可写 rw-,存放全局变量、静态变量等
- __LINKEDIT(链接信息段):只读 r--,存放链接器需要的信息,比如函数的名称和地址
dylib(动态链接库)
全称 Dynamic Library,后缀通常是.dylib,是 Mach-O 格式的一种。
- 动态库(.dylib):App运行时,系统才把这个库加载到内存里,可以被多个 App 的进程共享
- 静态库(.a):编译时,库代码会被完整拷贝进 App 可执行文件中,App 体积会变大,但运行时不需要找外部文件
dyld( 动态链接器)
全称 Dynamic Linker,是系统的一个辅助程序(也是一个 Mach-O 文件),运行在/usr/lib/dyld。
dyld 会将 App 依赖的动态库和 App 文件加载到内存。
dyld shared cache(动态库共享内存)
iOS 系统有成百上千个系统动态库,如果每个 App 启动,dyld 都要去磁盘中找到这些文件,打开它们读取header,验证签名,映射到内存... I/O操作会极其频繁,App启动速度变慢。
dyld shared cache 是一个巨型文件,Apple 在构建iOS系统时,把所有常见的系统动态库合并、预链接成一个单一的文件。
系统启动后,这个巨大的 Shared Cache 被映射到内存中,所有运行中的App都可以共享这一份 Shared Cache,dyld 在加载系统动态库时,直接识别到它在 Shared Cache里,直接用内存里的指针就行了。
image
images 在这里不是指图片,而是镜像。每个 App 都是以 images 为单位进行加载的。images 类型包括:
- executable:应用的二进制可执行文件;
- dylib:动态链接库;
- bundle:资源文件,属于不能被链接的 dylib,只能在运行时通过
dlopen()加载。
framework
framework 可以是动态库,也可以是静态库,是一个包含 dylib、boundle 和头文件的文件夹
冷启动
冷启动的整个过程是从用户点击图标开始到 AppDelegate 中的didFinishLaunchingWithOptions:方法执行完毕为止,并以执行main()函数的实际为分界点,分为 pre-main 和 main() 阶段。
pre-main 阶段
pre-mian 阶段指的是从用户唤起 App 到 main 函数执行之前的过程。

- 系统内核:
- 启动一个应用时,系统内核会创建一个新的进程,并为分配虚拟内存
- 把 App 对应的可执行文件加载到内存空间
- 把 dyld 加载到内存
- dyld
动态链接器接管进程- 加载动态库
- dyld 从主执行文件的 header 获取需要加载的所依赖的动态库列表,找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,因此需要加载的是动态库列表递归依赖的集合
- Rebase和Bind
由于 ASLR(Address Space Layout Randomization)的存在,image 会在新的随机地址上加载,和之前指针指向的地址会有一个偏差 slide ,因此 dyld 需要修正这个偏差,指向正确的地址- Rebase 重定位:在 imgae 内部调整指针的指向,性能消耗在IO。
- Bind 符号绑定:查询符号表,设置指向 image 外部的指针,性能消耗在 CPU 计算。
- ObjC setup
完成 Rebase 和Bind 之后,会通知 Runtime- Objc 类注册:读取二进制中的类列表,将其注册到Runtime表中
- Category 注册:将Category中的方法插入到主类的方法列表中
- Selector 唯一性检查
- Initializers
- 调用每个 Objc 类和分类中的 +load 函数
- 调用 C/C++ 中的构造器函数
- 创建非基本类型的 C++ 静态全局变
- 加载动态库
- dyld调用App的main()函数
ObjC setup 中 Runtime 注册的类是哪里的类?
- 注册:
- 系统库的类: UIView, NSString, NSArray 等。
- 三方库的类: AFNetworking, SDWebImage 里的类。
- 你自己写的类: MyViewController, MyModel 等。
- 原因:
- Objective-C 是一门动态语言。编译后的二进制文件(Mach-O)里,类只是"数据"。App 启动时,系统(dyld 和 Runtime)需要把这些"数据"读出来,在内存中构建一张巨大的"映射表"。
- 例如,我调用
[MyObject alloc]时,Runtime 必需在内存里查到MyObject对应的方法列表在哪里。要与类进行映射
了解了main()函数之前到加载过程,可以分析出一些影响因素:
- 动态库加载越多,启动越慢
- ObjC类、方法、分类越多、启动越慢
- ObjC的+load越多、启动越慢C的
- constructor函数越多,启动越慢
- C++静态对象越多,启动越慢
针对以上几点,我们可以做如下的优化:
在pre-main阶段:
- 重新梳理架构,减少不必要的内置动态库数量
- 代码瘦身:减少类、方法、分类的数量,合并一些功能、删除无效的类、方法和分类
- +load优化:尽量避免在类的+load方法中初始化,可以推迟到+initialize中进行
一般来说,当我们的项目规模不是很大的时候,,Pre-main(main函数执行前)阶段能挖掘的优化空间确实相对有限,因为没有几百个动态库,也没有几十万行的遗留代码。
首先来详细解析一下此阶段的操作。
Post-main 阶段
此阶段,就是从main()函数开始执行到didFinishLaunchingWithOptions 方法执行结束的耗时。
这个阶段是串行执行的,阻塞了主线程,任何耗时操作都会直接导致 App 启动变慢。
1. main() 函数入口
swift
int main(int argc, char * argv[]) {
@autoreleasepool {
// 调用 UIApplicationMain
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
这里几乎没有耗时,直接进入下一步。
2.UIApplication 系统接管
- 初始化
UIApplication单例对象 - 初始化
AppDelegate代理对象 - 建立 Main RunLoop 主线程的runloop
- 解析
Info.plist,加载Main.storyboard
3.application:willFinishLaunchingWithOptions:
这是 AppDelegate 的第一个回调,耗时极短
4.application:didFinishLaunchingWithOptions: ❗️耗时处
通常这里会发生什么:
- 日志/统计 SDK 初始化
- 广告 SDK 初始化
- 业务 SDK 初始化
- UI 配置: 设置 UINavigationBar 全局样式、各种 Appearance 代理
- 用户登录状态检查: 从本地读取 Token,判断是进登录页还是主页
- I/O 操作:读取 NSUserDefaults,读取本地缓存文件,读取数据库
5.RootViewController 的生命周期
didFinishLaunching 还没结束(或者刚结束),代码会走到 window.makeKeyAndVisible。此时会触发根控制器的加载:
- init (初始化)
- viewDidLoad (加载视图)❗️耗时处
- 很多人在这里进行网络请求、读取大量数据库数据、解压图片。
- viewWillAppear
- viewWillLayoutSubviews
6.首屏渲染
这才是用户眼中的"启动完成"。当 CATransaction 提交,GPU 完成渲染,屏幕上终于不再是 LaunchScreen,而是你的首页内容。
了解完 Post-Main 阶段的过程后,针对一般规模的项目,可以进行如下的优化:
一、先梳理 didFinishLaunchingWithOptions:中的任务,分为三类:
- A 类 :必需且紧急,不执行就崩溃 / UI没法看
- B 类:必需但不紧急,晚几秒启动也不迟
- C 类:非必需
1. 延迟化加载
不要在 didFinishLaunchingWithOptions: 中一下子全部初始化所有 SDK。可以在首屏渲染之后 / 需要用到的时候 再初始化。
例如:支付 SDK 可以在用户点击"支付"按钮那一刻再注册,或者在首页加载完 3 秒后再注册。
2. 多线程分流
有些 I/O 操作不得不做,那就不要阻塞主线程。切换到子线程去操作。
二、精简 viewDidLoad
- UI代码 -> 留着
- 数据操作 -> 异步处理
- 非首屏UI -> 懒加载
参考:
https://juejin.cn/post/6844904085108310024
https://tech.meituan.com/2018/12/06/waimai-ios-optimizing-startup.html