应用的启动过程
启动过程分解
应用启动的过程可分为两步:
t(总)= t1(
main函数
之前) + t2(main函数
之后)
t1:加载系统动态库和应用可执行文件;
t2:构建界面并完成渲染和展示:main函数
到-application:didFinishLaunchingWithOptions:
回调执行结束;
1、main 函数之前
main函数之前启动过程的简单总结:
- 系统内核
XNU
先读取应用的可执行文件,加载动态链接器dyld
; - 动态链接器初始化运行环境,加载程序相关依赖库,并对这些库进行链接;
- 接着调用每个依赖库的初始化方法(runtime在这一步被初始化);
- 初始化程序可执行文件。这时runtime会初始化所有类的类结构,调用所有的
+load
方法; - 最后
dyld
返回main函数
地址,main函数
被调用,来到我们的程序入口。
1.1.加载可执行文件
大致的过程是:
- 内核启动进程管理器,为我们的应用创建新的进程;
- 调用
load_machfile()
函数加载Mach-O
,这里会设置虚拟内存大小、设置ASLR
随机数;load_machfile()
内部继续调用parse_machfile()
函数对Mach-O
文件进行深度解析; parse_machfile
先将Mach-O
文件的所有的加载命令Load Commends
映射进内核的内存,再分三趟解析这些加载命令。应用的可执行文件和dyld
都是Mach-O
文件,parse_machfile()
在解析可执行文件时会继续调用load_dylinker()
来处理加载命令LC_LOAD_DYLINKER
;load_dylinker
内递归调用parse_machfile
解析dyld
,成功后dyld
开始加载共享库;- 解析完可执行二进制文件类型的
Mach-O
文件后(假设为A),会得到A的入口点;但线程并不会立刻进入到这个入口点。因为还要加载动态链接器dyld
,在load_dylinker()
中,dyld
会保存A的入口点,递归调用parse_machfile()
后,将线程入口点设为dyld
的入口点。dyld
完成加载库的工作之后,再将入口点设回A的入口点,程序启动完成。
1.2.dyld的工作流程
dyld 加载成功后会接管后续的启动任务:
- Load dylibs image;
- Rebase image;
- Bind image;
- ObjC Setup;
- Initializers;
i.Load dylibs image
dyld将可执行文件以及依赖的库递归地加载进内存,生成对应的镜像对象:
- 分析所依赖的动态库;
- 找到动态库的mach-o文件;
- 打开文件;
- 验证文件;
- 在系统核心注册文件签名;
- 对动态库的每一个segment调用mmap();
针对这一步骤的优化有:
- 减少非系统库的依赖;
- 合并非系统库;
- 使用静态资源,比如把代码加入主程序;
ii.链接镜像
对上面生成的镜像进行链接,主要是对镜像进行rebase/bind
。
由于ASLR的存在,每次启动时,可执行文件和动态链接库在虚拟内存中的加载地址都不固定,所以需要这2步来修复镜像中资源的指针。
- rebase 修复的是指向当前镜像内部的资源指针;
- bind 修复的是指向当前镜像外部的资源指针;
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。
优化该阶段的关键在于减少__DATA segment
中的指针数量:
- 减少 Objc 类数量, 减少 selector 数量;
- 减少 C++ 虚函数数量;
- 使用 Swift stuct,减少符号的数量;
iii.ObjC Setup
主要是调用各镜像的初始化方法:
- 注册Objc类 (class registration);
- 把category的定义插入方法列表 (category registration);
- 保证每一个selector唯一 (selctor uniquing);
iv.initializers
以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容:
- Objc的
+load()
函数; - C++的构造函数属性函数,形如 attribute((constructor)) void DoSomeInitializationWork();
- 非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) ,比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度;
至此,可执行文件和动态库中的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,由 runtime 管理。这之后 runtime 的那些方法(动态添加Class、swizzle等)才能生效。
整个事件由dyld主导,完成运行环境的初始化后,将二进制文件按格式加载到内存,动态链接依赖库,并由 runtime 加载成 objc 定义的结构,所有初始化工作结束后,dyld调用真正的main
函数。
冷启动与热启动:
- 热启动:如果程序刚刚被运行过,则程序代码会被 dyld 缓存,即使杀掉进程再次重启,加载时间也会相对快一点;
- 冷启动:如果长时间没有启动或者当前 dyld 的缓存已经被其他应用占据,那么这次启动所花费的时间就要长一点;
怎么衡量main()
之前的耗时呢?苹果官方提供了一种方法:在Xcode中,设置环境变量DYLD_PRINT_STATISTICS
和DYLD_PRINT_STATISTICS_DETAILS
后运行来查看耗时。
1 |
|
设置后再次启动APP即可在控制台看到日志:
1 |
|
2、main 函数之后
1、执行main函数;
2、执行UIApplicationMain函数:
1 |
|
3、检查Info.plist设置、创建显示主窗口:
1 |
|
优化启动时间:
main()
之前可优化点:
- 减少不必要的 framework;
- 合并或者删减一些OC类;
- 将不必须在+load方法中做的事情延迟到+initialize中;
- 删减一些无用的静态变量;
- 删减没有被调用到或者已经废弃的方法;
- 尽量不要用C++虚函数(创建虚函数表有开销)。
main()
及之后可优化点:
- 不使用xib,直接视用代码加载首页视图;
- release版本不要使用 NSLog 打印日志;
- NSUserDefaults中保存的内容不宜过多。
- 梳理应用启动时发送的所有网络请求,统一在异步线程请求;
相关参考:
#©MissionPeak-XNU、dyld源码分析Mach-O和动态库的加载过程(上)