如何精确度量 iOS App 的启动时间
iOS启动分为两个时间:
- pre-main时间
- main时间
一、pre-main时间检测
Xcode 提供了一个很赞的方法,只需要在 Edit scheme -> Run -> Arguments 中将环境变量 DYLD_PRINT_STATISTICS 设为 1,就可以看到 main 之前各个阶段的时间消耗
Total pre-main time: 341.32 milliseconds (100.0%)
dylib loading time: 154.88 milliseconds (45.3%)
rebase/binding time: 37.20 milliseconds (10.8%)
ObjC setup time: 52.62 milliseconds (15.4%)
initializer time: 96.50 milliseconds (28.2%)
slowest intializers :
libSystem.dylib : 4.07 milliseconds (1.1%)
libMainThreadChecker.dylib : 30.75 milliseconds (9.0%)
AFNetworking : 19.08 milliseconds (5.5%)
LDXLog : 10.06 milliseconds (2.9%)
Bigger : 7.05 milliseconds (2.0%)
还有一个方法获取更详细的时间,只需将环境变量 DYLD_PRINT_STATISTICS_DETAILS 设为 1 就可以。
total time: 2.8 seconds (100.0%)
total images loaded: 488 (471 from dyld shared cache)
total segments mapped: 61, into 24958 pages
total images loading time: 1.1 seconds (40.6%)
total load time in ObjC: 92.39 milliseconds (3.2%)
total debugger pause time: 794.39 milliseconds (28.2%)
total dtrace DOF registration time: 0.00 milliseconds (0.0%)
total rebase fixups: 921,005
total rebase fixups time: 109.77 milliseconds (3.9%)
total binding fixups: 694,265
total binding fixups time: 766.41 milliseconds (27.2%)
total weak binding fixups time: 9.05 milliseconds (0.3%)
total redo shared cached bindings time: 768.13 milliseconds (27.3%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 690.73 milliseconds (24.5%)
libSystem.B.dylib : 11.67 milliseconds (0.4%)
libBacktraceRecording.dylib : 12.06 milliseconds (0.4%)
libobjc.A.dylib : 6.09 milliseconds (0.2%)
libMainThreadChecker.dylib : 59.50 milliseconds (2.1%)
libViewDebuggerSupport.dylib : 7.66 milliseconds (0.2%)
libglInterpose.dylib : 286.97 milliseconds (10.2%)
libMTLCapture.dylib : 4.28 milliseconds (0.1%)
AWUnityFramework : 103.15 milliseconds (3.6%)
AiWayFashionCar : 365.65 milliseconds (12.9%)
total symbol trie searches: 1594338
total symbol table binary searches: 0
total images defining weak symbols: 63
total images using weak symbols: 133
1. 优化(dylib loading time):
在项目优化实践中,我们移除了一个没有必要的动态库,并将几个动态库合成为一个动态库,减少动态库数量
第一个阶段:pre-main time 中第一个阶段 dylib loading time : 动态库加载阶段
***注: ***
如何查看动态库的个数: Products 中.app中会有Frameworks文件夹 里面即App需要引入的动态库
举例:Flutter优化日志
这里区分两种方式加载Flutter:
区别:
1. 第一种方式会将flutter依赖的第三方插件做成pod子仓的形式直接引入的源码,
App.framework Flutter.framework
2. 第二种方式会将flutter依赖的第三方插件做成framework,之后将所有的framework做成pod仓库
App.framework, FlutterPluginRegistrant.framework ,shared_preferences.framework, wakelock.framework , FMDB.framework , flutter_boost.framework , sqflite.framework , webview_flutter.framework, Flutter.framework , path_provider.framework , video_player.framework
造成的后果是: 第二种主工程会引入很多framework,造成的影响是动态库加载时间变长
第一种:采取直接污染主工程方式: 由于日志较多,仅展示敏感数据
Total pre-main time: 685.23 milliseconds (100.0%) dylib loading time: 152.81 milliseconds (22.3%) rebase/binding time: 74.06 milliseconds (10.8%) ObjC setup time: 44.86 milliseconds (6.5%) initializer time: 413.48 milliseconds (60.3%) Total pre-main time: 980.53 milliseconds (100.0%) dylib loading time: 163.32 milliseconds (16.6%) rebase/binding time: 86.59 milliseconds (8.8%) ObjC setup time: 223.50 milliseconds (22.7%) initializer time: 507.11 milliseconds (51.7%)
第二种:采取pod仓库形式
Total pre-main time: 818.21 milliseconds (100.0%) dylib loading time: 243.54 milliseconds (29.7%) rebase/binding time: 72.09 milliseconds (8.8%) ObjC setup time: 39.28 milliseconds (4.8%) initializer time: 463.27 milliseconds (56.6%) Total pre-main time: 1.3 seconds (100.0%) dylib loading time: 270.75 milliseconds (20.3%) rebase/binding time: 69.74 milliseconds (5.2%) ObjC setup time: 282.78 milliseconds (21.2%) initializer time: 708.54 milliseconds (53.2%)
对比结果: dylib loading time 时间拉长了100多毫秒
技术选择:
- 第一种方式污染主工程: 可以看到结果是framework变少
- 第二种方式极少的污染主工程: 结果是framework变多
2. 优化(rebase/binding time):
这一阶段系统主要注册 Objc 类。所以,指针数量越少越好。这一步能做的优化有:
- 清理项目中无用的类
- 删减没有被调用到或者已经废弃的方法
- 删减一些无用的静态变量
核心思想是在进行动态库的重定位和绑定(Rebase/binding)(ASLR:dylib会被加载到随机地址,这个随机的地址跟代码和数据指向的旧地址会有偏差,dyld 需要修正这个偏差,做法就是将 dylib 内部的指针地址都加上这个偏移量) 过程中减少指针修正;
减少Objective-C类数量,减少分类,减少实例变量和函数(删除不用的类以及冗余代码,再深一点就是减少第三方工具的使用,可以查看源码,自己实现);
减少C++虚函数;
3.优化(ObjC setup time):
Objc Setup Time
这一步主要做了以下操作
注册Objc类 (class registration)
把category的定义插入方法列表 (category registration)
保证每一个selector唯一 (selctor uniquing)
前两部做好之后这一步就没有什么可以有优化的
4. 优化(initializer time):
第一个阶段:pre-main time 中第4个阶段 initializer time :
这一阶段 dyld开始运行程序的初始化函数,调用每个Obj类和分类的+load方法,
这一阶段,dyld 开始运行程序的初始化函数,调用每个 Objc 类和分类的 +load 方法,调用 C/C++ 中的构造器函数。initializer阶段执行完后,dyld 开始调用 main() 函数。在这一步,检查 +load 方法,尽量把事情推迟到 +initiailize 方法里执行。
- 使用initialize替代load方法
- 减少使用c/c++的attribute((constructor));推荐使用dispatch_once(),peathrd_once(), std:once()等方法
- 不要在初始化中创建线程
- 推荐使用swift
优化前
Total pre-main time: 731.83 milliseconds (100.0%)
dylib loading time: 250.62 milliseconds (34.2%)
rebase/binding time: 50.32 milliseconds (6.8%)
ObjC setup time: 37.81 milliseconds (5.1%)
initializer time: 393.07 milliseconds (53.7%)
slowest intializers :
libSystem.B.dylib : 6.20 milliseconds (0.8%)
libMainThreadChecker.dylib : 28.41 milliseconds (3.8%)
libglInterpose.dylib : 187.20 milliseconds (25.5%)
AiWayFashionCar : 244.98 milliseconds (33.4%)
优化后
Total pre-main time: 667.68 milliseconds (100.0%)
dylib loading time: 237.69 milliseconds (35.6%)
rebase/binding time: 53.64 milliseconds (8.0%)
ObjC setup time: 36.47 milliseconds (5.4%)
initializer time: 339.86 milliseconds (50.9%)
slowest intializers :
libSystem.B.dylib : 6.05 milliseconds (0.9%)
libMainThreadChecker.dylib : 28.70 milliseconds (4.2%)
libglInterpose.dylib : 152.15 milliseconds (22.7%)
AiWayFashionCar : 218.33 milliseconds (32.7%)
// 经全局查找看到有一个load方法里面执行了IO操作相关API, 优化之后 initializer time 有所优化
二、main 之后的时间度量
main 到 didFinishLaunching 结束或者第一个 ViewController 的viewDidAppear 都是作为 main 之后启动时间的一个度量指标。
这个时间统计直接打点计算就可以,不过当遇到时间较长需要排查问题时,只统计两个点的时间其实不方便排查,目前见到比较好用的方式就是为把启动任务规范化、粒子化,针对每个任务都有打点统计,这样方便后期问题的定位和优化。
以此记录优化启动日志