背景
项目功能越来越复杂化,影响到启动时间(主要是感觉自己菜,需要学习)
APP启动时都干了什么事
一般情况下,App 的启动分为冷启动和热启动,我们这里说的是优化冷启动时间。
冷启动是指, App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。
热启动是指 ,App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。
一般而言,App 的启动时间,指的是从用户点击 App 开始,到用户看到第一个界面之间的时间。总结来说,App 的启动主要包括三个阶段:
main() 函数执行前;
main() 函数执行后;
首屏渲染完成后。
1.main() 函数执行前
main() 函数执行前的阶段启动时间通过添加环境变量,可以打印出APP的启动时间分析:
(Edit scheme -->Run -->Arguments -->Environment Variables)
添加一项:Name为DYLD_PRINT_STATISTICS,Value为1,然后运行项目查看日志
Total pre-main time: 348.75 milliseconds (100.0%)
dylib loading time: 302.06 milliseconds (86.6%)
rebase/binding time: 126687488.9 seconds (135339814.9%)
ObjC setup time: 17.65 milliseconds (5.0%)
initializer time: 50.90 milliseconds (14.5%)
slowest intializers :
libSystem.B.dylib : 5.88 milliseconds (1.6%)
libMainThreadChecker.dylib : 32.44 milliseconds (9.3%)
在 main() 函数执行前,系统主要会做下面几件事情(对照上面代码看):
- 加载可执行文件(App 的.o 文件的集合);
- 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定;
- Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;
- 初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。
对应的启动速度优化:
- 减少动态库加载。每个库本身都有依赖关系,苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持 6 个非系统动态库合并为一个。
- 移除没有用到的类或者方法。
- +load() 方法的内容可以放到首屏渲染完成后再执行,或使用 +initialize() 方法替换掉。因为,在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。
- 控制 C++ 全局变量的数量。
2.main() 函数执行后启动时间
main() 函数执行后的阶段,指的是从 main() 函数执行开始,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成。
首页的业务代码都是要在这个阶段,也就是首屏渲染前执行的,主要包括了:
- 首屏初始化所需配置文件的读写操作;
- 首屏列表大数据的读取;
- 首屏渲染的大量计算等。
很多时候,开发者会把各种初始化工作都放到这个阶段执行,导致渲染完成滞后。更加优化的开发方式,应该是从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 启动必要的初始化功能,而哪些是只需要在对应功能开始使用时才需要初始化的。梳理完之后,将这些初始化功能分别放到合适的阶段进行。例如地图,支付,统计,推送等功能的注册,有些是必须放在appDelegate中注册,有些可以放在首屏渲染完成之后注册,或者有些可以放在子线程注册。
3.首屏渲染完成后
首屏渲染后的这个阶段,主要完成的是,非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等。从函数上来看,这个阶段指的就是截止到 didFinishLaunchingWithOptions 方法作用域内执行首屏渲染之后的所有方法执行完成。
这个阶段就是从渲染完成时开始,到 didFinishLaunchingWithOptions 方法作用域结束时结束。这个阶段用户已经能够看到 App 的首页信息了,所以优化的优先级排在最后。但是,那些会卡住主线程的方法还是需要最优先处理的,不然还是会影响到用户后面的交互操作。
功能级别的启动优化
功能级别的启动优化,就是要从 main() 函数执行后这个阶段下手。
优化的思路是:main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。
其他小优化
- 合并功能相似的类和扩展(Category)
由于Category的实现原理,和ObjC的动态绑定有很强的关系,实际上类的扩展是比较占用启动时间的。尽量可能合并一些扩展,并不是让你不使用扩展 - 清理无用的资源图片,压缩较大的资源图片
推荐使用LSUnusedResources查找无用的资源图片,使用ImageOptim压缩较大的资源图片 - 优化applicationWillFinishLaunching
需要在applicationWillFinishLaunching里处理的业务较多时,可以管理起这些任务
将不需要马上在applicationWillFinishLaunching执行的代码延后执行 - 优化rootViewController
rootViewController的加载,适当将某一级的childViewController或subviews延后加载
如果你的App可能会被后台拉起并冷启动,可考虑不加载rootViewController - 不使用xib,直接使用代码加载首页视图
- NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)
- 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
- 梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求
方法级别启动优化
检查首屏渲染完成前主线程上的耗时方法,将非刚需的耗时方法滞后或异步执行。耗时较长的方法主要发生在计算大量数据的情况下,例如加载、编辑、存储图片和文件等资源。
比如+load() 方法,一个耗时 4 毫秒,100 个就是 400 毫秒。ReactiveCocoa 框架每创建一个信号都有 6 毫秒的耗时。这样,稍不注意各种信号的创建就都被放在了首屏渲染完成前,进而导致 App 的启动速度大幅变慢等。
目前来看,对 App 启动速度的监控,主要有两种手段。
第一种方法是,定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。
Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式。开发类似工具成本不高,能够快速开发后集成到你的 App 中,以便在真实环境中进行检查。
对于定时时间间隔的控制
- 间隔长:会漏掉某些方法,从而导致检查出来的耗时不精确
- 间隔短:抓取堆栈这个方法本身调用过多会影响整体耗时,导致结果不准确
- 定时抓取主线程调用栈的方式精准度不够高,做参考足以
- 大神得出最合适时间为 0.01 秒,虽然导致许多运行速度快的方法监控误差,对整体耗时影响小
第二种方法是,对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时。
Hook:在原方法开始执行时换成执行其他指定的方法,或者在原有方法执行前后执行你指定的方法,来达到掌握和改变指定方法的目的。
hook objc_msgSend 这种方式的优点是非常精确,而缺点是只能针对 Objective-C 的方法。对c方法和block需要借助第三方框架处理,编写维护成本高。
检查方法耗时的工具
推荐使用工具SMCallTrace
友情提示:需要在SMCallTrace.m中打开第54行的注释。
自主实现时间检测工具
学习笔记,参考多位大神,感谢大神分享!