1. App的启动分为三个主要阶段:
main()函数执行前
main()函数执行后(从main函数执行,到设置self.window.rootViewController)
首屏渲染完成后(从设置self.window.rootViewController到didFinishLaunchWithOptions方法作用域结束)
main函数执行前,系统会做的事情:
加载可执行文件(App的.o文件集合)
加载动态链接库,进行rebase指针调整和bind符号绑定
Objc运行时的初始处理,包括Objc相关类注册、category注册、selector唯一性检查等
初始化,包括了执行+load()方法、attribute((constructor))修饰的函数的调用、创建C++静态全局变量。
main()函数执行后:
main()函数执行后的阶段,指的是从main()函数执行开始,到appDelegate的didFinishLaunchingWithOpentions方法里首屏渲染相关方法执行完成。
这里应该是从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是App启动必要的初始化功能,哪些是只需要在对应功能开始使用时才需要初始化的,将这些放到各自合适的阶段执行。
首屏渲染完成后:
首屏渲染后的这个阶段,指的是didFinishLaunchWithOptions方法作用域内执行首屏渲染之后的所有方法执行完成,即从 设置了self.window.rootViewController开始 到 didFinishLaunchWithOptions方法作用域 结束。
首屏渲染完成后用户就可以看到App的首页信息了,把这个阶段内卡住主线程的方法解决掉就可以了。
注解:
App启动后,首先加载可执行文件,然后加载dyld,然后加载所有依赖库,然后调用所有的+load(),然后调用main(),然后调用UIApplicationMain(),然后调用AppDelegate的代理didFinishLaunchWithOptions.
可执行文件是指Mach-O格式的文件,也就是App中所有.o文件的集合体,从这里可以获取dyld的路径,然后加载dyld。
dyld是指苹果的动态链接器,加载dyld后,就会去初始化运行环境,开启缓存策略,加载依赖库,并且会调用每一个依赖库的初始化方法,包括RunTime也是在这里被初始化的,当所有的依赖库都被初始化完成后,RunTime会对项目中所有的类进行类初始化,调用所有的+load()方法,最后dyld会返回main函数地址,然后main函数会被调用。
知晓上述的流程后,我们就明白为什么优化启动速度,要去减少动态库加载,要少用+load(),理论明白了之后,我们就要看看具体怎么做了。
动态库是指可以共享的代码文件、资源文件、头文件等的打包集合体。在Xcode->Targets->General->Link Binary With Libraries可以检查自己的库,
-
减少+load()的使用,将里面的内容放到渲染结束后去做,或者用+initialize()代替。+load()方法在main()调用前就会调用,而+initialize()方法是在类第一次收到消息后,才会调用,两者的区别可以参考这里
2.具体优化方法
(1)减少+load()的使用
使用+initialize()的方法代替+load(),注意把逻辑移动到+initialize()时,要注意避免+initialize()的重复调用问题,可以使用dispatch_once()让逻辑只执行一次。
(2)对多个动态库进行合并
苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持6个非系统动态库合并为一个。
(3)优化类、方法、全局变量
减少加载启动后不会去使用的类或方法;控制C++全局变量的数量
(4)功能级别的启动优化
main()开始执行后到首屏渲染完成前,只处理首屏相关的业务,其他的都放到首屏渲染完成后去做。
(5)方法级别的启动优化
首先检查首屏渲染完成前主线程上的耗时操作,将没必要的操作滞后或异步。通常耗时操作有:加载、编辑、存储图片和文件等资源。
3. 查看耗时
(1)查看Main()调用前花费的总时间
在Product->Scheme->Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS
设置为YES,就可以在控制台中查看main函数执行前总共花费的多长时间。
(2)查看加载了多少动态库
在Product->Scheme->Edit Scheme->Run->Diagnostics->Logging->勾选Dynamic Library Loads,就可以在控制台中查看本项目中加载的所有动态库(包括系统的和自己的)。
(3)查看Main函数启动后的耗时
main函数调用后的耗时,可以使用一些工具来监控,有一种非常笨但是很实用的方法,就是通过打点,在didFinishLaunchingWithOptions开始前打一个点,在App显示完成第一个界面再打一个点,计算两个点之间的耗时,就可以知道main函数调用后到界面显示出来的耗时了,但是这样只能笼统的知道总的耗时,并不能准确的知道时间花在了哪里。
如果想用这个打点法的话,推荐一个打点工具BLStopwatch
如果想准确知道时间都花在了哪里,推荐使用下面两种方法。
4. 监控App启动耗时,精准找出时间都花在了哪里,方便逐一优化
准确监控方法有两种:
定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。Xcode自带的Time Profiler就是用的这种方法。
对objc_msgSend方法进行hook来掌握所有方法的执行耗时。
根据这两种方法,分别实现两个工具,来监控耗时
由于能力有限,我只根据第一种方法做出来一个计算某个线程的耗时工具,放在了这里BSMonitorTimeTool,大致思路如下:
(1). 通过定时器,每隔0.01s,获取一次主线程的函数堆栈,将函数名称、函数地址、函数耗时模型化为TimeModel
,保存在callStackDict
中,其中key为函数地址,value为TimeModel
(2). 定时执行的回调中,每次都判断函数地址是否存在,如果已经存在此函数地址,就讲对应的TimeModel中的耗时增加0.01s;如果不存在此函数地址,就初始化一个TimeModel,并将时间设置为0.01s。
(3). 当主界面显示完成之后,输出此callStackDict
,即可查看主线程中每个方法的耗时
5. 欢迎大家指正错误,希望能够共同进步
本文章是参考了很多大佬的文章,欢迎各位前去膜拜
- 戴铭大佬的极客时间02
- 贝聊科技大佬的一次立竿见影的启动时间优化