搜索
您的当前位置:首页正文

iOS - 优化App冷启动速度

来源:二三娱乐

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.

  • dyld是指苹果的动态链接器,加载dyld后,就会去初始化运行环境,开启缓存策略,加载依赖库,并且会调用每一个依赖库的初始化方法,包括RunTime也是在这里被初始化的,当所有的依赖库都被初始化完成后,RunTime会对项目中所有的类进行类初始化,调用所有的+load()方法,最后dyld会返回main函数地址,然后main函数会被调用。

  • 知晓上述的流程后,我们就明白为什么优化启动速度,要去减少动态库加载,要少用+load(),理论明白了之后,我们就要看看具体怎么做了。

  • 动态库是指可以共享的代码文件、资源文件、头文件等的打包集合体。在Xcode->Targets->General->Link Binary With Libraries可以检查自己的库,

  • Main函数调用前
    Main函数调用后

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函数执行前总共花费的多长时间。

设置环境变量.png
控制台会输出pre-main的总时间.png
(2)查看加载了多少动态库

在Product->Scheme->Edit Scheme->Run->Diagnostics->Logging->勾选Dynamic Library Loads,就可以在控制台中查看本项目中加载的所有动态库(包括系统的和自己的)。


image.png
(3)查看Main函数启动后的耗时

main函数调用后的耗时,可以使用一些工具来监控,有一种非常笨但是很实用的方法,就是通过打点,在didFinishLaunchingWithOptions开始前打一个点,在App显示完成第一个界面再打一个点,计算两个点之间的耗时,就可以知道main函数调用后到界面显示出来的耗时了,但是这样只能笼统的知道总的耗时,并不能准确的知道时间花在了哪里。

如果想准确知道时间都花在了哪里,推荐使用下面两种方法。

4. 监控App启动耗时,精准找出时间都花在了哪里,方便逐一优化

准确监控方法有两种:
  1. 定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。Xcode自带的Time Profiler就是用的这种方法。

  2. 对objc_msgSend方法进行hook来掌握所有方法的执行耗时。

根据这两种方法,分别实现两个工具,来监控耗时

(1). 通过定时器,每隔0.01s,获取一次主线程的函数堆栈,将函数名称、函数地址、函数耗时模型化为TimeModel,保存在callStackDict中,其中key为函数地址,value为TimeModel

(2). 定时执行的回调中,每次都判断函数地址是否存在,如果已经存在此函数地址,就讲对应的TimeModel中的耗时增加0.01s;如果不存在此函数地址,就初始化一个TimeModel,并将时间设置为0.01s。

(3). 当主界面显示完成之后,输出此callStackDict,即可查看主线程中每个方法的耗时

5. 欢迎大家指正错误,希望能够共同进步

本文章是参考了很多大佬的文章,欢迎各位前去膜拜

  • 戴铭大佬的
  • 贝聊科技大佬的
Top