iOS基础学习-1-启动时间监控

1,377 阅读6分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

启动时间监控

关于启动时间监控,只需要搞明白两件事情就完了

  1. 启动时间说的是哪一段时间
  2. 这段时间如何监控

启动时间到底是哪一段时间

从用户角度看启动时间

从用户角度看,启动时间分为两种衡量方式:

  1. 从点击图标到首页数据加载完毕
    • 比如首页是加载一张网络图片,那么启动时间就是从点击应用图标到首页图片显示出来的这个时间
  2. 从点击图标到Launch Image消失后的第一帧出现
    • 比如同样是首页加载一张网络图片,那么启动时间就是从点击应用图标到看到首页的这个时间(那一刻图片还没加载出来,只能看到一个首页title之类的)

因为不同应用的首页加载数据量不同,所以采用第一点很难对齐,所以最好按照第二点来衡量

从代码层面看启动时间

从代码角度看,官方给出了一种计算方式

  • 开始时间为进程创建时间
  • 结束时间是第一个CA::Transaction::commit()
    • CA::Transaction::commit()是 Core Animation 提供的一种事务机制,把一组 UI 上的修改打包,一起发给 Render Server 渲染。什么意思呢,就是提交一组UI到GPU进行绘制

两种启动方式

应用有两种启动方式:

  1. 冷启动

    • 冷启动就是系统里面没有进程缓存信息时的启动,比如手机重启后第一次启动、或者应用杀掉后时间过长系统缓存中进程缓存已经被清理,之后的第一次启动也是冷启动
  2. 热启动

    • 热启动就是指系统中进程缓存还在时的启动,比如应用刚杀掉又立即点击应用进入

热启动因为使用了进程缓存所以速度会快一些,所以后续所说的启动一般指冷启动

启动时间如何监控

这里有两个部分:

  1. 启动过程的开始时间和结束时间如何获取
  2. 启动过程中各个阶段的时间如何获取

整体启动时间监控

启动过程的开始时间和结束时间如何获取

开始时间

点击应用图标后的第一步就是创建进程,所以开始时间就是进程创建时间 获取进程创建时间可以通过当前进程标识(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;
}

结束时间

刚刚也分析了结束时间有两种算法:

  1. Launch Image消失后的第一帧出现
  2. 第一个CA::Transaction::commit()执行
Launch Image消失后的第一帧出现
  • iOS 12 及以下:root viewController 的 viewDidAppear
  • iOS 13+:applicationDidBecomeActive
第一个CA::Transaction::commit()执行

这个方法我们无法直接拿到第一次执行的时间,但是可以通过其他事件拿到接近这个点的时间

image.png CFRunLoopPerformBlockCA::Transaction::commit()执行之前调用,kCFRunLoopBeforeTimersCA::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);
});

各个阶段的时间如何获取

总的启动时间有了,接下来就是计算各个阶段的启动时间,首先要知道启动过程中有哪些阶段,应用启动过程通常我们分三个阶段

  1. main 函数之前,pre-main阶段
  2. main 函数之后
  3. 首屏渲染完成之后

main 函数之前

main 函数之前的阶段指的是从点击图标到执行main函数之前,我们称为pre-main阶段,这个阶段主要做一下几个事情:

  1. 加载可执行文件(app的.o文件的集合)
  2. 加载动态连接库
  3. 进行rebase指针调整和bind符号绑定
  4. OC运行时的初始处理(OC相关类的注册,category注册,selector唯一性检查等)
  5. 初始化(执行+load()方法,attribute(constructor)修饰的函数的调用、创建C++静态全局变量)

这里先大致知道做了哪些事情,后续会对每个事情做详细介绍

pre-main阶段时间监控

Apple提供了一种测量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为1 。之后控制台会输出类似内容,我们可以清晰的看到

image.png 整个pre-main 是1.3秒,下面还有各个部分的具体时间

main 函数之后

这个阶段是指从main函数执行开始到appDelegatedidFinishLaunchingWithOptions方法里面首屏渲染相关方法执行完成, 即,从main函数执行到设置self.window.rootViewController执行完成的阶段。 main函数之后主要做一下事情:

  1. 首屏初始化所需配置文件的读写操作
  2. 首屏列表大数据的读取
  3. 首屏渲染的大量计算
main 函数之后的时间监控

根据这个阶段的执行过程可以得出:

  • 开始时间可以在main函数刚开始时测量
  • 结束时间可以在self.window.rootViewController方法之后测量

首屏渲染完成之后

这个阶段是指在首屏渲染完成后到 didFinishLaunchingWithOptions结束这段内的方法执行

  • 开始时间就是在self.window.rootViewController方法之后测量
  • 结束时间就是didFinishLaunchingWithOptions方法结束

以上是通过代码来测量启动时间,其实还可以通过工具来测量时间

1.使用time profiler 监控

2.使用 app launch工具测量

这里暂不介绍。 以上,欢迎大家评论交流。