pre-main阶段
对于pre-main阶段,Apple提供了一种测量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量DYLD_PRINT_STATISTICS 设为1 ,运行项目,控制台会有如下输出:
分析一下各个步骤都干了啥:
(1)Load dylibs
在每个动态库的加载过程中, dyld需要:
分析所依赖的动态库
找到动态库的mach-o文件
打开文件
验证文件
在系统核心注册文件签名
对动态库的每一个segment调用mmap()
通常的,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载。
针对这一步骤的优化有:
- 减少非系统库的依赖
- 合并非系统库
- 使用静态资源,比如把代码加入主程序
App开始启动后, 系统首先加载可执行文件(自身App的所有.o文件的集合),然后加载动态链接库dyld,dyld是一个专门用来加载动态链接库的库。
动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。 系统使用动态链接有几点好处:
代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份。
易于维护:由于被依赖的 lib 是程序执行时才链接的,所以这些 lib 很容易做更新,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升级直接换成libSystem.C.dylib 然后再替换替身就行了。
减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多。
(2)rebase/bind
在dylib的加载过程中,系统为了安全考虑,引入了ASLR(Address Space Layout Randomization)技术和代码签名。由于ASLR的存在,镜像(Image,包括可执行文件、dylib和bundle)会在随机的地址上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。
Rebase在前,Bind在后,Rebase做的是将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。
所以,指针数量越少越好。在这一步,我们可以做的优化有:
- 减少ObjC类(class)、方法(selector)、分类(category)的数量
- 减少C++虚函数的的数量(创建虚函数表有开销)
- 使用Swift structs(内部做了优化,符号数量更少)
(3)Objc setup
大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类,将分类插入到类的方法列表里,再检查每个selector的唯一性。
在这一步倒没什么优化可做的,Rebase/Bind阶段优化好了,这一步的耗时也会减少。
(4) Initializers
到了这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数(用attribute((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。
在这一步,我们可以做的优化有:
- 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize
- 减少构造器函数个数,在构造器函数里少做些事情
- 减少C++静态全局变量的个数
main()调用之后的加载时间
这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我们会创建应用的window,指定其rootViewController,调用window的makeKeyAndVisible方法让其可见。由于业务需要,我们会初始化各个二方/三方库,设置系统UI风格,检查是否需要显示引导页、是否需要登录、是否有新版本等,由于历史原因,这里的代码容易变得比较庞大,启动耗时难以控制。
所以,满足业务需要的前提下,didFinishLaunchingWithOptions在主线程里做的事情越少越好。在这一步,我们可以做的优化有:
- 梳理各个二方/三方库,找到可以延迟加载的库,做延迟加载处理,比如放到首页控制器的viewDidAppear方法里。
- 梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
- 避免复杂/多余的计算。
- 避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。
- 采用性能更好的API。
- 首页控制器用纯代码方式来构建。