冷启动(Cold launch)耗时才是我们需要测量的重要数据,为了准确测量冷启动耗时,测量前需要重启设备。在 main() 方法执行前测量是很难的,好在 dyld 提供了内建的测量方法:在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1。控制台输出的内容如下:
Total pre-main time: 228.41 milliseconds (100.0%)
dylib loading time: 82.35 milliseconds (36.0%)
rebase/binding time: 6.12 milliseconds (2.6%)
ObjC setup time: 7.82 milliseconds (3.4%)
initializer time: 132.02 milliseconds (57.8%)
slowest intializers :
libSystem.B.dylib : 122.07 milliseconds (53.4%)
CoreFoundation : 5.59 milliseconds (2.4%)
APP启动时间计算公式
App总启动时间 = t1(main()之前的加载时间) + t2(main()之后的加载时间)。
t1 = 系统dylib(动态链接库)和自身App可执行文件的加载
t2 = main方法执行之后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示
启动流程
main()调用之前加载过程
exec() 是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用 ASLR)。并将起始位置到 0x000000 这段范围的进程权限都标记为不可读写不可执行。如果是 32 位进程,这个范围至少是 4KB;对于 64 位进程则至少是 4GB。NULL 指针引用和指针截断误差都是会被它捕获。
dylib loading
从主执行文件的 header 获取到需要加载的所依赖动态库列表,而 header 早就被内核映射过。然后它需要找到每个 dylib,然后打开文件读取文件起始位置,确保它是 Mach-O 文件。接着会找到代码签名并将其注册到内核。然后在 dylib 文件的每个 segment 上调用 mmap()。应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以 dyld 所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载 100 到 400 个 dylib 文件,但大部分都是系统 dylib,它们会被预先计算和缓存起来,加载速度很快
main()函数调用之前我们可以优化的点有:
不使用xib,直接视用代码加载首页视图。
NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,这个影响需要评估,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)。
每次用NSLog方式打印会隐式的创建一个Calendar, 仅仅针对内测版输出log。
梳理应用启动时发送的所有网络请求,统一在异步线程请求。
并行初始化各个业务。
优化方案
减少framework引用
删除无用类,无用函数
减少+load 函数使用,使用 +initialize 来替代 +load
main()调用之后, 优化内容
思路
launcherImage图片尽量小,实测这个大小会影响启动速度
Splash 不要Xib,直接用代码尽量简单
将需要执行的处理,放入不同的block内,并发到不同的queue中进行。
提供串行队列,执行有依赖的逻辑
提供group,对彼此依赖不明确,但需要整体执行完成后,进行处理的业务,提供dispatch_group功能满足需求。
对于MainThread有需要的业务,提供mainThread 支持