这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战
启动时间监控
关于启动时间监控,只需要搞明白两件事情就完了
- 启动时间说的是哪一段时间
- 这段时间如何监控
启动时间到底是哪一段时间
从用户角度看启动时间
从用户角度看,启动时间分为两种衡量方式:
- 从点击图标到首页数据加载完毕
- 比如首页是加载一张网络图片,那么启动时间就是从点击应用图标到首页图片显示出来的这个时间
- 从点击图标到Launch Image消失后的第一帧出现
- 比如同样是首页加载一张网络图片,那么启动时间就是从点击应用图标到看到首页的这个时间(那一刻图片还没加载出来,只能看到一个首页title之类的)
因为不同应用的首页加载数据量不同,所以采用第一点很难对齐,所以最好按照第二点来衡量
从代码层面看启动时间
从代码角度看,官方给出了一种计算方式
- 开始时间为进程创建时间
- 结束时间是第一个
CA::Transaction::commit()CA::Transaction::commit()是 Core Animation 提供的一种事务机制,把一组 UI 上的修改打包,一起发给 Render Server 渲染。什么意思呢,就是提交一组UI到GPU进行绘制
两种启动方式
应用有两种启动方式:
-
冷启动
- 冷启动就是系统里面没有进程缓存信息时的启动,比如手机重启后第一次启动、或者应用杀掉后时间过长系统缓存中进程缓存已经被清理,之后的第一次启动也是冷启动
-
热启动
- 热启动就是指系统中进程缓存还在时的启动,比如应用刚杀掉又立即点击应用进入
热启动因为使用了进程缓存所以速度会快一些,所以后续所说的启动一般指冷启动
启动时间如何监控
这里有两个部分:
- 启动过程的开始时间和结束时间如何获取
- 启动过程中各个阶段的时间如何获取
整体启动时间监控
启动过程的开始时间和结束时间如何获取
开始时间
点击应用图标后的第一步就是创建进程,所以开始时间就是进程创建时间
获取进程创建时间可以通过当前进程标识(NSProcessInfo\processIdentifier),读取进程信息内的进程创建时间(__p_starttime)为启动时间
#import <sys/sysctl.h>
#import <mach/mach.h>
+ (NSTimeInterval)processStartTime
{ // 单位是毫秒
struct kinfo_proc kProcInfo;
if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) {
return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000000.0;
} else {
NSAssert(NO, @"无法取得进程的信息");
return 0;
}
}
+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
{
int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
size_t size = sizeof(*procInfo);
return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}
结束时间
刚刚也分析了结束时间有两种算法:
- Launch Image消失后的第一帧出现
- 第一个
CA::Transaction::commit()执行
Launch Image消失后的第一帧出现
- iOS 12 及以下:root viewController 的 viewDidAppear
- iOS 13+:applicationDidBecomeActive
第一个CA::Transaction::commit()执行
这个方法我们无法直接拿到第一次执行的时间,但是可以通过其他事件拿到接近这个点的时间
CFRunLoopPerformBlock在CA::Transaction::commit()执行之前调用,kCFRunLoopBeforeTimers在CA::Transaction::commit()执行之后调用
- iOS13(含)以上的系统采用
runloop中注册一个kCFRunLoopBeforeTimers的回调获取到的 App 首屏渲染完成的时机更准确。
//注册kCFRunLoopBeforeTimers回调
CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopActivity activities = kCFRunLoopAllActivities;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopBeforeTimers) {
NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];
NSLog(@"runloop beforetimers launch end:%f",stamp);
CFRunLoopRemoveObserver(mainRunloop, observer, kCFRunLoopCommonModes);
}
});
CFRunLoopAddObserver(mainRunloop, observer, kCFRunLoopCommonModes);
- iOS13 以下的系统采用
CFRunLoopPerformBlock方法注入 block 获取到的 App 首屏渲染完成的时机更准确。
//注册block
CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){
NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];
NSLog(@"runloop block launch end:%f",stamp);
});
各个阶段的时间如何获取
总的启动时间有了,接下来就是计算各个阶段的启动时间,首先要知道启动过程中有哪些阶段,应用启动过程通常我们分三个阶段
main函数之前,pre-main阶段main函数之后- 首屏渲染完成之后
main 函数之前
main 函数之前的阶段指的是从点击图标到执行main函数之前,我们称为pre-main阶段,这个阶段主要做一下几个事情:
- 加载可执行文件(app的.o文件的集合)
- 加载动态连接库
- 进行rebase指针调整和bind符号绑定
- OC运行时的初始处理(OC相关类的注册,category注册,selector唯一性检查等)
- 初始化(执行
+load()方法,attribute(constructor)修饰的函数的调用、创建C++静态全局变量)
这里先大致知道做了哪些事情,后续会对每个事情做详细介绍
pre-main阶段时间监控
Apple提供了一种测量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为1 。之后控制台会输出类似内容,我们可以清晰的看到
整个pre-main 是1.3秒,下面还有各个部分的具体时间
main 函数之后
这个阶段是指从main函数执行开始到appDelegate的didFinishLaunchingWithOptions方法里面首屏渲染相关方法执行完成,
即,从main函数执行到设置self.window.rootViewController执行完成的阶段。
main函数之后主要做一下事情:
- 首屏初始化所需配置文件的读写操作
- 首屏列表大数据的读取
- 首屏渲染的大量计算
main 函数之后的时间监控
根据这个阶段的执行过程可以得出:
- 开始时间可以在main函数刚开始时测量
- 结束时间可以在
self.window.rootViewController方法之后测量
首屏渲染完成之后
这个阶段是指在首屏渲染完成后到 didFinishLaunchingWithOptions结束这段内的方法执行
- 开始时间就是在
self.window.rootViewController方法之后测量 - 结束时间就是
didFinishLaunchingWithOptions方法结束
以上是通过代码来测量启动时间,其实还可以通过工具来测量时间
1.使用time profiler 监控
2.使用 app launch工具测量
这里暂不介绍。 以上,欢迎大家评论交流。