前言
做 iOS 启动优化时,我发现一个特别常见的问题:
很多项目都在说"优化冷启动",但真要问一句:
你们的启动耗时,到底是从哪里开始算,到哪里结束算?
有人把 didFinishLaunching 当终点。
有人把首页 viewDidAppear 当终点。
还有人直接靠肉眼:"感觉打开挺快的"。
问题是,这几种口径都不够完整。
因为从用户点击 App,到真正看到首屏,中间其实走了很长一条链路:
- 进程启动
dyld装载- Runtime 初始化
maindidFinishLaunching- 首页控制器创建
- 首帧提交
- 首帧真正显示到屏幕
如果你只盯着其中一个点,很容易得出错误结论。
比如:
didFinishLaunching很快,但首页还是白屏了 300msviewDidAppear已经到了,但用户其实还没真正看到像素上屏- 冷启动数据还没分析明白,热启动和后台恢复又混在一起了
所以我后来自己整理了一套启动监控方案,并抽成了一个类: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:mainT2:didFinishLaunchingT3:首帧提交T4:首帧显示
这样你就可以得到几段很有价值的指标:
pre-main = T1 - T0mainToDidFinishLaunching = T2 - T1mainToFirstFrameCommit = T3 - T1mainToFirstFrameDisplay = T4 - T1coldLaunchTotal = T4 - T0
这几个指标的意义非常清晰:
pre-main看启动前置成本main -> didFinishLaunching看初始化链路main -> firstFrameDisplay看真正接近用户感知的启动体验coldLaunchTotal看一次冷启动的整体耗时
换句话说,启动监控不是只记一个时间点,而是应该把整条链路拆出来。
三、首帧显示怎么监控,才更接近真实用户体验?
这也是整套方案里最核心的一点。
我最终采用的是:
首页 viewDidAppear + 下一帧 CADisplayLink
为什么这么做?
因为这是一个在工程上非常实用的近似方案。
第一步:在首页 viewDidAppear 记录"页面已进入可见态"
这一步说明:
- 首页控制器已经完成切换
- 页面开始出现在显示链路中
但到这里还不能算"首帧真正上屏"。
第二步:再等一次 CADisplayLink
CADisplayLink 会跟随屏幕刷新节奏回调。
如果你在 viewDidAppear 之后挂一个一次性的 CADisplayLink,那么它第一次回调时,通常可以近似认为:
上一轮提交的内容已经进入显示周期了。
这时记录的时间点,会比单纯的 viewDidAppear 更接近"用户真的看见首屏"的时刻。
第三步:如果还想再细一点,可以加上 CATransaction completionBlock
这样就能把"首帧提交"和"首帧显示"分开:
firstFrameCommitfirstFrameDisplay
这两个指标拆开后,排查问题会非常舒服:
- 如果提交很早、显示很晚,可能是渲染/主线程/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 没有只做冷启动,而是把模式拆成了:
coldhotbackground_resume
这样最后拿到的数据是可以分场景分析的,而不是全部混成一个平均值。
这点在线上治理里非常重要。
因为你经常会看到:
- 冷启动其实没问题
- 真正差的是后台恢复
- 或者某一版热启动异常抖动
如果不分模式,这些问题很容易被埋掉。
七、BRAppLaunchMonitor 里几个核心指标分别代表什么?
这里把几个关键字段单独解释一下。
estimatedPreMainMS
表示估算的 pre-main 耗时。
适合看什么:
- 动态库装载成本
+load是否过重- 静态初始化是否过多
- 启动前置链路是否有异常增长
mainToDidFinishLaunchingMS
表示:
从 main 到 didFinishLaunching 的耗时
这段通常反映的是:
- 启动初始化任务
- SDK 注册
- 配置中心
- 路由准备
- 首页启动前置依赖
如果这一段很大,说明问题更可能出在初始化链路,而不是渲染链路。
mainToFirstFrameDisplayMS
这是我最看重的指标之一。
它代表:
从 main 到用户真正看到首帧的耗时
相比只看 didFinishLaunching,这个值更接近用户真实感知。
相比只看 viewDidAppear,这个值又更接近真正上屏。
如果你问我线上最值得长期监控哪一个,我会优先选它。
coldLaunchTotalMS
这个值等于:
estimatedPreMainMS + mainToFirstFrameDisplayMS
也就是:
一次完整冷启动从进程创建到首帧显示的大致总耗时
如果你要做启动大盘,这个指标非常适合作为"总耗时"来展示。
cycleToFirstFrameDisplayMS
这个指标很适合统一不同模式下的用户感知。
它表示:
当前这一轮启动/恢复周期,到首帧显示的耗时
也就是说:
- 冷启动能看
- 热启动能看
- 后台恢复也能看
这是它非常适合线上埋点的原因。
八、接入为什么不做 swizzle?
很多人做这种监控,第一反应是直接 swizzle:
viewDidAppeardidBecomeActive- 甚至首页路由方法
这样当然可以减少接入成本,但我最后没有这么做。
原因很简单:
1. 启动监控是基础能力,但首屏是谁是业务语义
比如一个 App 启动时可能先经过:
- 广告页
- 隐私页
- 引导页
- 登录页
- 跳转中间页
如果完全靠 Hook 自动判断"首屏",很容易打错点。
而一旦首屏定义错了,整条数据链路都会偏。
2. 显式接入更利于工程治理
我更倾向这种方式:
main显式打点AppDelegate显式打点- 真正首页控制器显式通知
它的优点是:
- 口径清晰
- 责任边界明确
- 排查问题时可解释性强
- 不容易和其他 Runtime 行为互相污染
对于基础设施类能力,我更看重稳定性和可解释性,而不是"看起来零成本"。
九、这套方案能解决什么问题?
当你把启动拆成这些阶段之后,很多问题会一下子变得非常清楚。
场景一:estimatedPreMainMS 明显偏高
优先排查:
- 动态库数量
+load- 静态初始化
- Runtime 注册成本
- 二进制体积和布局
如果你在做二进制重排,这个指标也很适合作为辅助观察项。
场景二:mainToDidFinishLaunchingMS 偏高
优先排查:
- 启动阶段同步 IO
- 重型 SDK 初始化
- 配置拉取阻塞
- 数据库启动成本
- 首页前置依赖过多
场景三:mainToFirstFrameDisplayMS 偏高,但 didFinishLaunching 还好
优先排查:
- 首页控制器构建
- Auto Layout 压力
- 首图解码
- 首屏列表初始化
- 主线程长任务
- 首帧前做了过多绘制
场景四:后台恢复慢
优先排查:
- 前后台状态恢复
- 页面恢复逻辑
- 激活后重新拉取数据
- 页面刷新策略
- 前后台切换动画
十、这套方案的边界也要说清楚
技术方案再好,也一定有边界。
1. pre-main 是估算值
这一点必须明确。
它适合:
- 线上监控
- 趋势分析
- 阶段拆解
它不适合:
- 替代 Instruments 做极致精确诊断
如果你要做更底层的启动分析,还是建议配合:
- Instruments
DYLD_PRINT_STATISTICSos_signpost- 启动阶段日志拆段
2. 技术首帧不等于业务首屏完成
这点也非常重要。
技术首帧表示的是:
- 页面已经显示出来了
但很多时候,用户真正觉得"页面出来了",还取决于:
- 首屏接口是否返回
- 列表首批数据是否 ready
- 首图是否解码完成
- 骨架屏是否消失
- 核心按钮是否可交互
所以在真实项目里,我通常会把指标拆成两层:
- 技术首帧
- 业务首屏 ready
前者看渲染链路,后者看用户体验闭环。
3. 首屏是谁,必须先对齐
如果启动链路里夹了:
- 广告页
- 引导页
- 登录拦截页
- 隐私页
那么你必须先定义清楚:
到底哪个页面才是业务首屏。
否则埋点本身就没有统一口径。
十一、如果要真正上线上,我会怎么上报?
如果把这套方案落到监控平台,我建议至少保留这些字段:
launch_modeestimated_pre_main_msmain_to_did_finish_launching_msmain_to_first_frame_display_mscold_launch_total_mscycle_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