应用启动流程剖析

每次App点开后的那几秒钟,背后到底发生了什么?

我做性能优化这些年,发现很多团队只盯着业务代码里的viewDidLoad,却忽略了系统在调用main()之前已经走了好长一段路。你想想看,从用户手指点到屏幕响应,内核、动态链接器、Runtime,一层层接力——任何一个环节慢了,启动时间就上去了。今天咱们就把这段路拆开看看。

核心观点:启动速度优化的第一步,是对整个流程的“线路图”了然于心。

操作系统加载过程

用户点击图标那一刻,进程还没出生呢。

  • 内核创建进程:fork + execve,分配虚拟内存、文件描述符,载入Mach-O文件头部。
  • 映射主可执行文件:内核按LC_SEGMENT命令把代码段、数据段映射到内存,但此时还没加载任何库。
  • 启动动态链接器:Mach-O里有个LC_LOAD_DYLINKER,内核会去找/usr/lib/dyld,把它加载到进程空间,并跳转到dyld入口。

这个过程听起来快,但如果你App有大量segment或page fault,这里就可能产生几百微妙的延迟。我遇到过一款游戏,因为一个未使用的Segment占了200MB,导致冷启动时系统要花30ms额外映射——直接在syscall层就输了。

实战检查:用vmmap看进程内存布局,关注__TEXT,__DATA等段的虚拟地址和驻留页数。

main函数前的工作

dyld跑起来以后,活儿才开始变细。我个人习惯把这个阶段称为“无主之地”——代码还没执行,但开销已经客观存在了。

1. 递归加载动态库

dyld解析主二进制所有的LC_LOAD_DYLIB,一层层往下找依赖。每个库都要做:

  • 文件映射(mmap)
  • 符号查找(绑定外部符号)
  • 执行初始化函数(+load、C++静态构造、__attribute__((constructor)))

2. 运行时初始化

Objective-C Runtime会在+load调用前做内部准备:

  • 读取各个类的__objc_classlist段,注册到分类表
  • 注册所有Category
  • 解析Protocol
  • 初始化自动释放池(objc_autoreleasePoolPush)

这里有个常见陷阱——+load里的代码会显著推迟main。我曾经接手一个项目,几百个+load都在做map预填充,硬生生吃了200ms。后来改成+initialize懒加载,冷启动时间直接砍掉15%。

避坑指南:千万不要在+load里做臃肿的初始化,特别是网络请求、磁盘读写、数据库建表。它跑在dyld线程,优先级极高,卡住就卡住全局。

Application/AppDelegate初始化阶段

等dyld忙完,main()终于被调用了。然后UIApplicationMain接管,干这几件事:

  1. 根据Info.plist里的NSPrincipalClass创建UIApplication实例
  2. 根据NSMainNibFile或UISceneStoryboard加载storyboard
  3. 创建AppDelegate实例,但不调用application:didFinishLaunchingWithOptions:——真正干活在RunLoop跑起来后

注意,这里有个容易被忽略的点:storyboard或xib里的初始视图会在UIApplicationMain期间被解档。如果你的RootViewController在xib里引用了大量图片、字体,那这部分时间也会被算进启动耗时。我在一个直播App里发现,仅一个启动页的xib就加载了12张全屏背景图(每张2MB),导致Main阶段多了80ms。

application:didFinishLaunchingWithOptions:执行时,整个UIWindow树已经搭好。大部分开发者的优化都堆在这里——SDK初始化、埋点启动、UI设置……但别忘了,前面这些系统流程已经跑完了,有些开销改不了,只能提前预知。

一句话总结:启动是系统工程,而非一个函数的事。
应用启动流程总览 1. 内核创建进程 2. dyld启动 3. 映射Mach-O 4. 递归加载动态库 5. +load & Runtime初始化 6. UIApplicationMain 7. didFinishLaunching ⚠️ 这里才开始跑 咱们的业务代码 内核态 用户态 内存映射

上面这张图串起了整个冷启动链路。你可以把它贴在团队墙上,走读代码时对着看——哪一步超时了,就锚定哪个阶段去排查。我个人习惯在main()前后打印时间戳,配合os_signpost,把整个流程拆成可观测的片段。

实用技巧:Xcode的“Time Profiler”在系统调用层面有符号,能直接看到dyld::initializeMainExecutable、load_images等函数耗时。跑一遍instruments,你就能抓到真正的瓶颈。

好,今天我们把启动的骨架画清楚了。下次聊到具体的打点工具和OSSignpost集成时,你会更容易理解数据从哪来、往哪去。