iOS冷启动监控Demo

前言

做 iOS 启动优化时,我发现一个特别常见的问题:

很多项目都在说"优化冷启动",但真要问一句:

你们的启动耗时,到底是从哪里开始算,到哪里结束算?

有人把 didFinishLaunching 当终点。

有人把首页 viewDidAppear 当终点。

还有人直接靠肉眼:"感觉打开挺快的"。

问题是,这几种口径都不够完整。

因为从用户点击 App,到真正看到首屏,中间其实走了很长一条链路:

  • 进程启动
  • dyld 装载
  • Runtime 初始化
  • main
  • didFinishLaunching
  • 首页控制器创建
  • 首帧提交
  • 首帧真正显示到屏幕

如果你只盯着其中一个点,很容易得出错误结论。

比如:

  • didFinishLaunching 很快,但首页还是白屏了 300ms
  • viewDidAppear 已经到了,但用户其实还没真正看到像素上屏
  • 冷启动数据还没分析明白,热启动和后台恢复又混在一起了

所以我后来自己整理了一套启动监控方案,并抽成了一个类:BRAppLaunchMonitor。

这篇文章就想把这套思路完整讲清楚。

不是只讲"怎么埋点",而是讲清楚:

  • 为什么很多启动监控口径不准
  • 首帧完成到底应该怎么定义
  • pre-main、post-main、首帧显示怎么拆
  • 冷启动 / 热启动 / 后台恢复怎么统一看
  • 最后怎么落成一个真正能在线上跑的监控类

一、为什么很多"启动耗时"其实没那么准?

先说结论:

didFinishLaunching 不是启动完成。

viewDidAppear 也不是严格意义上的首帧上屏。

这两个点都很重要,但都不能单独代表"用户已经看到首页"。

1. didFinishLaunching 为什么不够?

因为它更多代表的是:

  • App 主流程初始化完成
  • UIApplication 启动链路走到一个阶段
  • 但首页未必已经真正构建完成
  • 更别说渲染和显示了

也就是说,你把启动结束定义在这里,天然会漏掉:

  • 首页视图构建
  • Auto Layout
  • 首屏列表初始化
  • 图片解码
  • 首帧提交和显示

如果项目里首页比较重,这段漏掉的时间可能还不小。


2. viewDidAppear 为什么也不够?

这是很多人容易忽略的点。

viewDidAppear 表示当前页面已经进入显示流程,但它不等于:

用户一定已经看到了最终那一帧。

因为在 UIKit / Core Animation 这条链路里,还要经过:

  • 主线程提交 Layer Tree
  • Render Server 处理
  • 等待下一次 VSync
  • 最终显示到屏幕

所以如果你只在 viewDidAppear 打点,拿到的其实更像是:

"首屏已经进入可见态"

而不是:

"首帧已经真正显示到屏幕"


二、那启动耗时到底该怎么定义?

如果从工程角度做启动监控,我比较推荐拆成下面 5 个关键时间点:

  • T0:进程启动
  • T1:main
  • T2:didFinishLaunching
  • T3:首帧提交
  • T4:首帧显示

这样你就可以得到几段很有价值的指标:

  • pre-main = T1 - T0
  • mainToDidFinishLaunching = T2 - T1
  • mainToFirstFrameCommit = T3 - T1
  • mainToFirstFrameDisplay = T4 - T1
  • coldLaunchTotal = T4 - T0

这几个指标的意义非常清晰:

  • pre-main 看启动前置成本
  • main -> didFinishLaunching 看初始化链路
  • main -> firstFrameDisplay 看真正接近用户感知的启动体验
  • coldLaunchTotal 看一次冷启动的整体耗时

换句话说,启动监控不是只记一个时间点,而是应该把整条链路拆出来。


三、首帧显示怎么监控,才更接近真实用户体验?

这也是整套方案里最核心的一点。

我最终采用的是:

首页 viewDidAppear + 下一帧 CADisplayLink

为什么这么做?

因为这是一个在工程上非常实用的近似方案。

第一步:在首页 viewDidAppear 记录"页面已进入可见态"

这一步说明:

  • 首页控制器已经完成切换
  • 页面开始出现在显示链路中

但到这里还不能算"首帧真正上屏"。


CADisplayLink 会跟随屏幕刷新节奏回调。

如果你在 viewDidAppear 之后挂一个一次性的 CADisplayLink,那么它第一次回调时,通常可以近似认为:

上一轮提交的内容已经进入显示周期了。

这时记录的时间点,会比单纯的 viewDidAppear 更接近"用户真的看见首屏"的时刻。


第三步:如果还想再细一点,可以加上 CATransaction completionBlock

这样就能把"首帧提交"和"首帧显示"分开:

  • firstFrameCommit
  • firstFrameDisplay

这两个指标拆开后,排查问题会非常舒服:

  • 如果提交很早、显示很晚,可能是渲染/主线程/VSync 问题
  • 如果连提交都很晚,那大概率是首屏构建本身太重

四、所以我做了一个 BRAppLaunchMonitor

基于上面的思路,我把启动链路收敛成了一个监控类:BRAppLaunchMonitor。

它做的事情并不复杂,但很实用:

  • 在 main 记录主流程起点
  • 估算 pre-main
  • 在 didFinishLaunching 记录初始化阶段结束
  • 在首页 viewDidAppear + CADisplayLink 记录首帧显示
  • 同时监听前后台切换,兼顾热启动 / 后台恢复

也就是说,它不只看"冷启动",还试图统一回答一个问题:

这次 App 从进入活跃状态到真正可见,用户到底等了多久?


五、pre-main 怎么拿?

严格来说,pre-main 很难在线上埋点里做到绝对精准。

因为这段发生在 main 之前,包含了很多系统和运行时动作,比如:

  • dyld 装载
  • ObjC Runtime 初始化
  • +load
  • 静态构造
  • C/C++ 全局对象初始化

业务代码很难无侵入地精确包住整段过程。

所以 BRAppLaunchMonitor 里,我采用的是一个很实用的方案:

  • 获取进程启动时间
  • 再减去 main 进入时间
  • 得到一个 estimatedPreMainMS

这里要强调一下,它是:

工程估算值

不是 Apple 官方工具级别的"绝对精确值"。

但它已经足够回答很多工程问题了,比如:

  • 启动慢,到底慢在 pre-main 还是 post-main
  • 二进制膨胀、动态库增多、+load 过多是不是已经影响到启动
  • 做了二进制重排之后,是否对启动阶段有收益

对线上监控来说,这样的精度已经很有价值。


六、为什么要把冷启动、热启动、后台恢复分开?

很多启动监控方案只关注冷启动,但真实线上体验并不是只有冷启动会慢。

还有两类情况也很常见:

  • 热启动
  • 后台恢复

比如用户切后台一会儿,再切回来:

  • 页面要恢复状态
  • 有的列表要刷新
  • 有的模块要重新绘制
  • 有的首页还会重新发请求

这些过程如果卡顿,用户一样会觉得"App 打开得慢"。

所以 BRAppLaunchMonitor 没有只做冷启动,而是把模式拆成了:

  • cold
  • hot
  • background_resume

这样最后拿到的数据是可以分场景分析的,而不是全部混成一个平均值。

这点在线上治理里非常重要。

因为你经常会看到:

  • 冷启动其实没问题
  • 真正差的是后台恢复
  • 或者某一版热启动异常抖动

如果不分模式,这些问题很容易被埋掉。


七、BRAppLaunchMonitor 里几个核心指标分别代表什么?

这里把几个关键字段单独解释一下。

estimatedPreMainMS

表示估算的 pre-main 耗时。

适合看什么:

  • 动态库装载成本
  • +load 是否过重
  • 静态初始化是否过多
  • 启动前置链路是否有异常增长

mainToDidFinishLaunchingMS

表示:

从 main 到 didFinishLaunching 的耗时

这段通常反映的是:

  • 启动初始化任务
  • SDK 注册
  • 配置中心
  • 路由准备
  • 首页启动前置依赖

如果这一段很大,说明问题更可能出在初始化链路,而不是渲染链路。


mainToFirstFrameDisplayMS

这是我最看重的指标之一。

它代表:

从 main 到用户真正看到首帧的耗时

相比只看 didFinishLaunching,这个值更接近用户真实感知。

相比只看 viewDidAppear,这个值又更接近真正上屏。

如果你问我线上最值得长期监控哪一个,我会优先选它。


coldLaunchTotalMS

这个值等于:

  • estimatedPreMainMS + mainToFirstFrameDisplayMS

也就是:

一次完整冷启动从进程创建到首帧显示的大致总耗时

如果你要做启动大盘,这个指标非常适合作为"总耗时"来展示。


cycleToFirstFrameDisplayMS

这个指标很适合统一不同模式下的用户感知。

它表示:

当前这一轮启动/恢复周期,到首帧显示的耗时

也就是说:

  • 冷启动能看
  • 热启动能看
  • 后台恢复也能看

这是它非常适合线上埋点的原因。


八、接入为什么不做 swizzle?

很多人做这种监控,第一反应是直接 swizzle:

  • viewDidAppear
  • didBecomeActive
  • 甚至首页路由方法

这样当然可以减少接入成本,但我最后没有这么做。

原因很简单:

1. 启动监控是基础能力,但首屏是谁是业务语义

比如一个 App 启动时可能先经过:

  • 广告页
  • 隐私页
  • 引导页
  • 登录页
  • 跳转中间页

如果完全靠 Hook 自动判断"首屏",很容易打错点。

而一旦首屏定义错了,整条数据链路都会偏。


2. 显式接入更利于工程治理

我更倾向这种方式:

  • main 显式打点
  • AppDelegate 显式打点
  • 真正首页控制器显式通知

它的优点是:

  • 口径清晰
  • 责任边界明确
  • 排查问题时可解释性强
  • 不容易和其他 Runtime 行为互相污染

对于基础设施类能力,我更看重稳定性和可解释性,而不是"看起来零成本"。


九、这套方案能解决什么问题?

当你把启动拆成这些阶段之后,很多问题会一下子变得非常清楚。

场景一:estimatedPreMainMS 明显偏高

优先排查:

  • 动态库数量
  • +load
  • 静态初始化
  • Runtime 注册成本
  • 二进制体积和布局

如果你在做二进制重排,这个指标也很适合作为辅助观察项。


场景二:mainToDidFinishLaunchingMS 偏高

优先排查:

  • 启动阶段同步 IO
  • 重型 SDK 初始化
  • 配置拉取阻塞
  • 数据库启动成本
  • 首页前置依赖过多

场景三:mainToFirstFrameDisplayMS 偏高,但 didFinishLaunching 还好

优先排查:

  • 首页控制器构建
  • Auto Layout 压力
  • 首图解码
  • 首屏列表初始化
  • 主线程长任务
  • 首帧前做了过多绘制

场景四:后台恢复慢

优先排查:

  • 前后台状态恢复
  • 页面恢复逻辑
  • 激活后重新拉取数据
  • 页面刷新策略
  • 前后台切换动画

十、这套方案的边界也要说清楚

技术方案再好,也一定有边界。

1. pre-main 是估算值

这一点必须明确。

它适合:

  • 线上监控
  • 趋势分析
  • 阶段拆解

它不适合:

  • 替代 Instruments 做极致精确诊断

如果你要做更底层的启动分析,还是建议配合:

  • Instruments
  • DYLD_PRINT_STATISTICS
  • os_signpost
  • 启动阶段日志拆段

2. 技术首帧不等于业务首屏完成

这点也非常重要。

技术首帧表示的是:

  • 页面已经显示出来了

但很多时候,用户真正觉得"页面出来了",还取决于:

  • 首屏接口是否返回
  • 列表首批数据是否 ready
  • 首图是否解码完成
  • 骨架屏是否消失
  • 核心按钮是否可交互

所以在真实项目里,我通常会把指标拆成两层:

  • 技术首帧
  • 业务首屏 ready

前者看渲染链路,后者看用户体验闭环。


3. 首屏是谁,必须先对齐

如果启动链路里夹了:

  • 广告页
  • 引导页
  • 登录拦截页
  • 隐私页

那么你必须先定义清楚:

到底哪个页面才是业务首屏。

否则埋点本身就没有统一口径。


十一、如果要真正上线上,我会怎么上报?

如果把这套方案落到监控平台,我建议至少保留这些字段:

  • launch_mode
  • estimated_pre_main_ms
  • main_to_did_finish_launching_ms
  • main_to_first_frame_display_ms
  • cold_launch_total_ms
  • cycle_to_first_frame_display_ms

再补一些维度:

  • 设备型号
  • iOS 版本
  • App 版本
  • 首屏页面标识
  • 是否命中广告/引导页
  • 首屏是否依赖网络数据

这样后续你分析启动问题时,就不只是"平均启动慢了",而是能进一步回答:

  • 哪个版本慢
  • 哪个机型慢
  • 哪个首屏慢
  • 是冷启动慢还是后台恢复慢
  • 是初始化慢还是渲染慢

这才是启动治理真正有用的地方。


十二、总结

回到最开始的问题:

iOS 启动监控到底应该怎么做?

我的答案是:

  • 不要只看 didFinishLaunching
  • 不要只看 viewDidAppear
  • 不要把冷启动、热启动、后台恢复混成一份数据
  • 也不要把"技术首帧"和"业务首屏"混为一谈

更实用的一套工程方案应该是:

  • 用进程启动时间估算 pre-main
  • 用 main 作为业务可控起点
  • 用 didFinishLaunching 拆初始化链路
  • 用 viewDidAppear + CADisplayLink 近似首帧显示
  • 用生命周期区分冷启动、热启动、后台恢复

而 BRAppLaunchMonitor 本质上做的,就是把这条原本模糊的"启动体验"链路,变成一套可测、可拆、可优化的数据结构。

很多时候,优化启动最难的不是"怎么调",而是"你根本不知道问题发生在哪"。

没有监控,优化只是一种感觉。

有了监控,优化才真正变成工程。


代码如下:

BRAppLaunchMetrics.h 复制代码
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, BRAppLaunchMode) {
    BRAppLaunchModeUnknown = 0,
    BRAppLaunchModeCold,
    BRAppLaunchModeHot,
    BRAppLaunchModeBackgroundResume,
};

FOUNDATION_EXPORT NSString *BRStringFromAppLaunchMode(BRAppLaunchMode mode);

@interface BRAppLaunchMetrics : NSObject

@property (nonatomic, assign, readonly) BRAppLaunchMode launchMode;

@property (nonatomic, assign, readonly) NSTimeInterval estimatedPreMainMS;
@property (nonatomic, assign, readonly) NSTimeInterval mainToDidFinishLaunchingMS;
@property (nonatomic, assign, readonly) NSTimeInterval mainToFirstViewDidAppearMS;
@property (nonatomic, assign, readonly) NSTimeInterval mainToFirstFrameCommitMS;
@property (nonatomic, assign, readonly) NSTimeInterval mainToFirstFrameDisplayMS;
@property (nonatomic, assign, readonly) NSTimeInterval coldLaunchTotalMS;

@property (nonatomic, assign, readonly) NSTimeInterval cycleToFirstFrameDisplayMS;

@end

typedef void(^BRAppLaunchReportBlock)(BRAppLaunchMetrics *metrics);

@interface BRAppLaunchMonitor : NSObject

@property (nonatomic, copy, nullable) BRAppLaunchReportBlock reportBlock;

+ (instancetype)sharedMonitor;

- (void)markMainEntry;
- (void)markDidFinishLaunching;
- (void)notifyFirstScreenViewDidAppear;

- (void)notifyApplicationDidEnterBackground;
- (void)notifyApplicationWillEnterForeground;
- (void)notifyApplicationDidBecomeActive;

- (void)reset;

@end

NS_ASSUME_NONNULL_END
BRAppLaunchMetrics.m 复制代码
#import "BRAppLaunchMonitor.h"
#import <QuartzCore/QuartzCore.h>
#import <sys/sysctl.h>
#import <unistd.h>

static inline CFTimeInterval BRALNowMediaTime(void) {
    return CACurrentMediaTime();
}

static inline NSTimeInterval BRALNowWallTime(void) {
    return [[NSDate date] timeIntervalSince1970];
}

static inline NSTimeInterval BRALDeltaMS(CFTimeInterval start, CFTimeInterval end) {
    if (start <= 0 || end <= 0 || end < start) {
        return 0;
    }
    return (end - start) * 1000.0;
}

static NSTimeInterval BRALProcessStartWallTime(void) {
    int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
    struct kinfo_proc procInfo;
    size_t size = sizeof(procInfo);
    memset(&procInfo, 0, sizeof(procInfo));

    if (sysctl(mib, 4, &procInfo, &size, NULL, 0) != 0 || size == 0) {
        return 0;
    }

    struct timeval startTime = procInfo.kp_proc.p_un.__p_starttime;
    return (NSTimeInterval)startTime.tv_sec + ((NSTimeInterval)startTime.tv_usec / 1000000.0);
}

NSString *BRStringFromAppLaunchMode(BRAppLaunchMode mode) {
    switch (mode) {
        case BRAppLaunchModeCold: return @"cold";
        case BRAppLaunchModeHot: return @"hot";
        case BRAppLaunchModeBackgroundResume: return @"background_resume";
        default: return @"unknown";
    }
}

@interface BRAppLaunchMetrics ()

@property (nonatomic, assign, readwrite) BRAppLaunchMode launchMode;
@property (nonatomic, assign, readwrite) NSTimeInterval estimatedPreMainMS;
@property (nonatomic, assign, readwrite) NSTimeInterval mainToDidFinishLaunchingMS;
@property (nonatomic, assign, readwrite) NSTimeInterval mainToFirstViewDidAppearMS;
@property (nonatomic, assign, readwrite) NSTimeInterval mainToFirstFrameCommitMS;
@property (nonatomic, assign, readwrite) NSTimeInterval mainToFirstFrameDisplayMS;
@property (nonatomic, assign, readwrite) NSTimeInterval coldLaunchTotalMS;
@property (nonatomic, assign, readwrite) NSTimeInterval cycleToFirstFrameDisplayMS;

@end

@implementation BRAppLaunchMetrics
@end

@interface BRAppLaunchMonitor ()

@property (nonatomic, assign) BRAppLaunchMode currentMode;

@property (nonatomic, assign) NSTimeInterval processStartWallTime;
@property (nonatomic, assign) NSTimeInterval mainEntryWallTime;
@property (nonatomic, assign) CFTimeInterval mainEntryMediaTime;
@property (nonatomic, assign) CFTimeInterval didFinishLaunchingTime;

@property (nonatomic, assign) CFTimeInterval cycleStartMediaTime;
@property (nonatomic, assign) CFTimeInterval firstViewDidAppearTime;
@property (nonatomic, assign) CFTimeInterval firstFrameCommitTime;
@property (nonatomic, assign) CFTimeInterval firstFrameDisplayTime;

@property (nonatomic, assign) BOOL hasBecomeActiveOnce;
@property (nonatomic, assign) BOOL isInBackground;
@property (nonatomic, assign) BOOL hasReportedCurrentCycle;

@property (nonatomic, strong, nullable) CADisplayLink *displayLink;

@end

@implementation BRAppLaunchMonitor

+ (instancetype)sharedMonitor {
    static BRAppLaunchMonitor *monitor;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        monitor = [[BRAppLaunchMonitor alloc] init];
    });
    return monitor;
}

- (void)markMainEntry {
    [self ensureMainThread];

    if (self.mainEntryMediaTime > 0) {
        return;
    }

    self.processStartWallTime = BRALProcessStartWallTime();
    self.mainEntryWallTime = BRALNowWallTime();
    self.mainEntryMediaTime = BRALNowMediaTime();

    self.currentMode = BRAppLaunchModeCold;
    self.cycleStartMediaTime = self.mainEntryMediaTime;
    self.hasReportedCurrentCycle = NO;
}

- (void)markDidFinishLaunching {
    [self ensureMainThread];

    if (self.mainEntryMediaTime <= 0) {
        [self markMainEntry];
    }
    if (self.didFinishLaunchingTime <= 0) {
        self.didFinishLaunchingTime = BRALNowMediaTime();
    }
}

- (void)notifyFirstScreenViewDidAppear {
    [self ensureMainThread];

    if (self.currentMode != BRAppLaunchModeCold || self.hasReportedCurrentCycle) {
        return;
    }
    if (self.firstViewDidAppearTime > 0) {
        return;
    }

    self.firstViewDidAppearTime = BRALNowMediaTime();

    [CATransaction begin];
    __weak typeof(self) weakSelf = self;
    [CATransaction setCompletionBlock:^{
        __strong typeof(weakSelf) self = weakSelf;
        if (!self || self.hasReportedCurrentCycle) {
            return;
        }
        if (self.firstFrameCommitTime <= 0) {
            self.firstFrameCommitTime = BRALNowMediaTime();
        }
        [self armDisplayLinkIfNeeded];
    }];
    [CATransaction commit];

    dispatch_async(dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf) self = weakSelf;
        if (!self || self.hasReportedCurrentCycle) {
            return;
        }
        if (self.firstFrameCommitTime <= 0) {
            self.firstFrameCommitTime = BRALNowMediaTime();
        }
        [self armDisplayLinkIfNeeded];
    });
}

- (void)notifyApplicationDidEnterBackground {
    [self ensureMainThread];
    self.isInBackground = YES;
}

- (void)notifyApplicationWillEnterForeground {
    [self ensureMainThread];

    if (!self.hasBecomeActiveOnce) {
        return;
    }

    [self prepareNewCycleWithMode:BRAppLaunchModeBackgroundResume];
}

- (void)notifyApplicationDidBecomeActive {
    [self ensureMainThread];

    if (!self.hasBecomeActiveOnce) {
        self.hasBecomeActiveOnce = YES;
        self.isInBackground = NO;
        return;
    }

    if (self.isInBackground) {
        self.isInBackground = NO;
        if (self.currentMode != BRAppLaunchModeBackgroundResume) {
            [self prepareNewCycleWithMode:BRAppLaunchModeBackgroundResume];
        }
    } else {
        [self prepareNewCycleWithMode:BRAppLaunchModeHot];
    }

    self.firstFrameCommitTime = BRALNowMediaTime();
    [self armDisplayLinkIfNeeded];
}

- (void)reset {
    [self ensureMainThread];

    [self.displayLink invalidate];
    self.displayLink = nil;

    self.currentMode = BRAppLaunchModeUnknown;

    self.processStartWallTime = 0;
    self.mainEntryWallTime = 0;
    self.mainEntryMediaTime = 0;
    self.didFinishLaunchingTime = 0;

    self.cycleStartMediaTime = 0;
    self.firstViewDidAppearTime = 0;
    self.firstFrameCommitTime = 0;
    self.firstFrameDisplayTime = 0;

    self.hasBecomeActiveOnce = NO;
    self.isInBackground = NO;
    self.hasReportedCurrentCycle = NO;
}

- (void)prepareNewCycleWithMode:(BRAppLaunchMode)mode {
    [self.displayLink invalidate];
    self.displayLink = nil;

    self.currentMode = mode;
    self.cycleStartMediaTime = BRALNowMediaTime();
    self.firstViewDidAppearTime = 0;
    self.firstFrameCommitTime = 0;
    self.firstFrameDisplayTime = 0;
    self.hasReportedCurrentCycle = NO;
}

- (void)armDisplayLinkIfNeeded {
    if (self.displayLink || self.hasReportedCurrentCycle) {
        return;
    }

    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)handleDisplayLink:(CADisplayLink *)displayLink {
    if (self.hasReportedCurrentCycle) {
        return;
    }

    self.hasReportedCurrentCycle = YES;
    self.firstFrameDisplayTime = BRALNowMediaTime();

    [displayLink invalidate];
    self.displayLink = nil;

    BRAppLaunchMetrics *metrics = [self buildMetrics];
    if (self.reportBlock) {
        self.reportBlock(metrics);
    } else {
        NSLog(@"[BRAppLaunchMonitor] mode=%@ preMain=%.2fms main->display=%.2fms coldTotal=%.2fms cycle->display=%.2fms",
              BRStringFromAppLaunchMode(metrics.launchMode),
              metrics.estimatedPreMainMS,
              metrics.mainToFirstFrameDisplayMS,
              metrics.coldLaunchTotalMS,
              metrics.cycleToFirstFrameDisplayMS);
    }
}

- (BRAppLaunchMetrics *)buildMetrics {
    BRAppLaunchMetrics *metrics = [[BRAppLaunchMetrics alloc] init];
    metrics.launchMode = self.currentMode;

    if (self.currentMode == BRAppLaunchModeCold) {
        NSTimeInterval estimatedPreMainMS = 0;
        if (self.processStartWallTime > 0 && self.mainEntryWallTime > 0 && self.mainEntryWallTime >= self.processStartWallTime) {
            estimatedPreMainMS = (self.mainEntryWallTime - self.processStartWallTime) * 1000.0;
        }

        metrics.estimatedPreMainMS = estimatedPreMainMS;
        metrics.mainToDidFinishLaunchingMS = BRALDeltaMS(self.mainEntryMediaTime, self.didFinishLaunchingTime);
        metrics.mainToFirstViewDidAppearMS = BRALDeltaMS(self.mainEntryMediaTime, self.firstViewDidAppearTime);
        metrics.mainToFirstFrameCommitMS = BRALDeltaMS(self.mainEntryMediaTime, self.firstFrameCommitTime);
        metrics.mainToFirstFrameDisplayMS = BRALDeltaMS(self.mainEntryMediaTime, self.firstFrameDisplayTime);
        metrics.coldLaunchTotalMS = metrics.estimatedPreMainMS + metrics.mainToFirstFrameDisplayMS;
    }

    metrics.cycleToFirstFrameDisplayMS = BRALDeltaMS(self.cycleStartMediaTime, self.firstFrameDisplayTime);
    return metrics;
}

- (void)ensureMainThread {
    NSAssert([NSThread isMainThread], @"BRAppLaunchMonitor must be used on main thread.");
}

@end
相关推荐
光影少年1 小时前
为什么 JavaScript 中 0.1 + 0.2 !== 0.3,如何让其相等?
前端·javascript·算法
掘金酱2 小时前
[稀土掘金 × 火山引擎] AI用量周榜冲刺赛|获奖名单公示
前端·人工智能·后端
泡泡oO2 小时前
“如果你还在用Superpowers,那我不要和你说话”
前端·后端·全栈
维克兜率天2 小时前
趋势过滤:避免被“假突破“反复打脸
前端
8年区块链老兵3 小时前
一篇文章教会你:用区块高度读懂比特币链上的每一笔交易
前端
xixichensh3 小时前
前端转做AI程序员第一步——开发一个AI流式对话Demo
前端
小羊没烦恼!3 小时前
系统内部模块(子系统)之间的耦合以及模块(子系统)划分
java·开发语言·前端·数据库·算法·c#
Leslie1653 小时前
聚合不只是 GROUP BY:HashAggregate 与 SortAggregate 的内存账本
前端
ITOM运维行者4 小时前
PHP性能监控怎么做?从响应时间到慢函数的6个关键指标
前端·javascript·后端