启动耗时分析,一般我们会以main函数作为分割点,main之前和main之后main之前称为per-main 阶段。这个由dyld给你反馈应用的耗时。main之后由开发者自己检测。我们可以从main开始打点,到第一个页面显示为止。
通过实际的调试,我们得到各个函数的调用顺序如下:
启动页
main()
UIApplicationMain()
willFinishLaunchingWithOptions()
didFinishLaunchingWithOptions()
loadView()
viewDidLoad()
applicationDidBecomeActive()
启动页是在main()函数调用之前出来的,main()是程序的入口,里面调用了UIApplicationMain()。当App从didFinishLaunchingWithOptions()返回的时候,实际的UI立刻开始加载,但是在applicationDidBecomeActive()这个回调完成之前,UI即使已经初始化,但仍旧被阻塞着。
总的启动时间T包括main()调用之前的pre-main timeT0,
加上从main()到applicationDidBecomeActive()的时间T1。
pre-main阶段耗时
main函数之前的检测苹果提供了支持,具体配置方式如图
----首先进入Edit Scheme
----然后配置的 key 为:DYLD_PRINT_STATISTICS
----然后我们再运行项目,该项目 pre-main 的耗时就会在控制台输出。
各阶段耗时分析
dylib loading time 动态库载入耗时
载入动态库,这个过程中,会去装载app使用的动态库,而动态库之间有它自己的依赖关系,所以会消耗时间去查找和读取。
优化建议:
1.系统的动态库,做了优化。所以从效率的角度来说,尽可能使用系统库;
2.而对于开发者定义导入的动态库(dynamically linked shared library),则需要在花费更多的时间。Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们;
3.在性能上出发将动态库编译成静态库也会优化这部分时间;
rebase/binding time 修正符号和绑定符号耗时
Rebase:在镜像(MachO文件)内部调整指针的指向,针对mach-o在加载到内存中不是固定的首地址(ASLR)这一现象做数据修正的过程。
iOS4.3后引入了 ASLR ,MachO会被加载到随机地址,这个随机的地址跟代码和数据指向的旧地址会有偏差。dyld 需要修正这个偏差,做法就是将 dylib 内部的指针地址都加上这个偏移量。
binding:将指针指向镜像(MachO文件)外部的内容,binding就是将这个二进制调用的外部符号进行绑定的过程。
优化建议:
1.核心思想是在进行动态库的重定位和绑定(Rebase/binding)过程中减少指针修正;
2.减少Objective-C类数量,减少分类,减少实例变量和函数(删除不用的类以及冗余代码,再深一点就是减少第三方工具的使用,可以查看源码,自己实现);
3.减少C++虚函数;
4.多使用Swift结构体(推荐使用swift)
ObjC setup time OC类注册的耗时
主要做以下几件事来完成Objc Setup:
1、读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
2、注册 Objc 类,ObjC Runtime 需要维护一张映射类名与类的全局表。当加载一个 MachO 时,它定义的所有的类都需要被注册到这个全局表中;
3、读取 protocol 以及 category 的信息,把category的定义插入方法列表 (category registration),
优化建议:
1.不刻意的去减少几个类,但是可以避免浪费;
2 随着项目的不断迭代,很多模块和方法已经被废弃但是却一直留存在项目中,导致项目越来越臃肿;
3.我们可以使用一些工具来查找项目中没有被用到的文件。从而达到优化;
initializer time 其他初始化,如上图,细分为其他的几个部分
1、Objc的+load()函数
2、C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
优化建议:
1.我们能做的就是将不必须在+load方法中做的事情延迟到+initialize中;
2.这是因为+load方法是在app启动的时候就被调用,而+initialize方法则是在Class第一次使用的时候才调用,相当于是懒加载了。可以把+load中的代码移到initialize中,并结合dispatch_once来防止重复调用;
3.但是我们项目中只有在使用method swizzling的时候会在+load中调用方法。所以这一点也没什么好优化的;
pre-main阶段耗时总结:
1. 动态库加载越多,启动越慢
2. ObjC类,方法越多,启动越慢
3. ObjC的+load越多,启动越慢
4. C的constructor函数越多,启动越慢
5. C++静态对象越多,启动越慢
main()到applicationDidBecomeActive()的阶段耗时
我们可以使用Xcode自带工具Instruments里面的Time Profiler来获取,也可以在main()的第一句和applicationDidBecomeActive()的最后一句加上获取时间的代码CFAbsoluteTimeGetCurrent(),
工具通过Xcode工具栏中Product->Profile(command+i)可以启动,(也可以通过Xcode->Open Developer Tool->Instruments)启动后界面如下:
选择Time Profiler,打开后如图:
点击左上角红色按钮运行,勾选左下角Call Tree中Separate Thread和Hide System Libraries,等到第一个页面显示出来的之后,点击左上角暂停按钮,下面就会统计出每个步骤的耗时情况。这个时候我们就可以很容易得到启动时间T1。
针对这块时间的耗时优化总结:
我们通过Time Profiler拿到每个步骤的耗时之后,右下角的 Heaviest Trace 可查看比较消耗CPU的代码,双击点击进去可查看到对应的代码,进行修改。有些操作可以延后执行,或者异步执行等,这些需要根据自己的业务逻辑在处理。