App 启动时都做了什么?
一般情况下,App 的启动分为冷启动和热启动。
- 冷启动是指, App 点击启动前,它的进程不在系统里,需要
系统新创建一个进程
分配给它启动的情况。这是一次完整的启动过程。(用户感知到的启动慢,就是主线程上存在较多的耗时方法,如:大文件的读写操作、在渲染周期中执行了大量的计算等等) - 热启动是指 ,App 在冷启动后用户将 App 退后台,
App 的进程还在系统里
的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。
以下是App冷启动的相关优化:
一、App的启动主要包括三个阶段:
点击App,main()函数执行前,main()函数执行后,首屏渲染完成后,启动完成
1. main()函数执行前;
- 加载可执行文件(App的.o文件的集合)
- 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定;
- Objc运行时的初始处理,包括Objc相关类的注册、category注册、selector唯一性检查等等
- 初始化,包括执行+load()方法、attribute((constructor))修饰的函数调用、创建C++静态全局变量。
在这个阶段的优化启动速度,可以: - 减少动态库加载。每个库本身有依赖关系,数量较多时,可以合并多个动态库,苹果建议最多使用6个非系统动态库
- 减少加载启动后首屏不使用的类或者方法。
- +load()方法里的内容放在首屏渲染完成后再执行,或使用+initialize()方法替换掉。【+load()里进行运行时方法替换操作会有4毫秒的耗时,+load()方法的越多执行将拖慢App启动速度】
- 减少C++全局变量的数量。
2. main()函数执行后;
指的是从main()函数执行开始,到appDelegate的didFinishLaunchingWithOptions方法里首屏渲染相关方法执行完成。
首页的业务代码都在这个阶段执行的,包括了:
- 首屏初始化所需配置文件的读写操作;
- 首屏列表大数据的读取;
- 首屏渲染的大量计算等。
很多时候,开发者会把各种初始化工作都放到这个阶段执行,导致渲染完成滞后。
更加优化的开发方式,应该是
- 从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 启动必要的初始化功能,而哪些是只需要在对应功能开始使用时才需要初始化的。梳理完之后,将这些初始化功能分别放到合适的阶段进行。
3. 首屏渲染完成后。
主要完成的是,非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等。从函数上来看,这个阶段指的是设置了self.window.rootViewController开始到didFinishLaunchWithOptions方法作用域 结束,用户可以看到首页的信息了,然后处理一下会卡主线程的方法,来避免影响用户对首页的操作。
二、具体优化方法
1. 减少+load()的使用
使用+initialize()的方法代替+load(),注意把逻辑移动到+initialize()时,需避免+initialize()的重复调用问题,可以使用dispatch_once()让逻辑只执行一次。或者使用- viewDidLoad()
2. 对多个动态库进行合并,非系统动态库不超6个
3. 优化类、方法、全局变量
减少加载启动后不使用的类或方法;控制C++全局变量的数量
4.功能级别的启动优化
main()开始执行后到首屏渲染完成前,只处理首屏相关的业务,其他的都放到首屏渲染完成后去做。
5.方法级别的启动优化
首先检查首屏渲染完成前主线程上的耗时操作,将没必要的操作滞后或异步。通常耗时操作有:加载、编辑、存储图片和文件等资源。
三、查看耗时
1.查看Main()调用前的总耗时
设置环境变量
在Product->Scheme->Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS
设置为YES,就可以在控制台中查看main函数执行前总共花费的多长时间。控制台会输出:Total pre-main time: 388.69 milliseconds (100.0%)
还有dylib、rebase/binding、Objc Setup、initializer、dylib的加载时间。
2. 查看加载了多少动态库
在Xcode中选择工程文件,然后选择Targets,里面有Build Phases,这里面有Link Binary With Libraries(20 items),这里包含了系统和自己的动态库。
3.查看Main函数启动后的耗时
可以使用一些工具来监控,也可以在didFinishLaunchingWithOptions开始前打印时间,在App显示完成第一个界面再打印一个时间,计算时间差,就是main函数调用后到界面显示出来的总耗时了,有一个好用的工具:BLStopwatch
想准确知道时间花在了哪里,可以使用以下方法:
4.监控App启动耗时,精准找出方法,进行优化
两种准确监控的方法
1.定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。Xcode自带的Time Profile就是这样。
2.对objc_msgSend方法进行hook来掌握所有方法的执行耗时。
实现耗时监控工具思路:
(1). 通过定时器,每隔0.01s,获取一次主线程的函数堆栈,将函数名称、函数地址、函数耗时模型化为TimeModel,保存在callStackDict中,其中key为函数地址,value为TimeModel
(2). 定时执行的回调中,每次都判断函数地址是否存在,如果已经存在此函数地址,就讲对应的TimeModel中的耗时增加0.01s;如果不存在此函数地址,就初始化一个TimeModel,并将时间设置为0.01s。
(3). 当主界面显示完成之后,输出此callStackDict,即可查看主线程中每个方法的耗时
参考:戴铭-极客时间02,贝聊科技-一次立杆见影的启动时间优化,冰风v落叶-iOS优化App冷启动速度