结论先行
在这两个候选时间点里——
application:didFinishLaunchingWithOptions:
执行结束- 主线程第一次进入 idle(RunLoop
kCFRunLoopBeforeWaiting
)
若你只能二选一,以「主线程首次 idle」作为 启动结束 更合理。它比 didFinishLaunchingWithOptions:
更贴近用户真正“看到并可操作界面”的时刻,而误差仍可控制;同时埋点方案也比较稳健,可跨 UIKit / SwiftUI / SceneDelegate 使用。
为什么 didFinishLaunching…
偏早?
- Apple 的
MXAppLaunchMetric
只统计到didFinishLaunch()
(即didFinishLaunching…
)为止,Uber 等大型团队实测后认为这比他们旧的 首帧渲染 指标“缩水”了一截,需要再补一段自定义测量来涵盖 UI 绘制流程citeturn10view0。 didFinishLaunching…
返回时,UI 还没走完viewDidLoad → viewWillAppear → first draw → viewDidAppear
,因此用户仍在看 LaunchScreen 或白屏。把它当作结束点会 乐观低估 启动耗时。
为什么选 “RunLoop 首次 idle” 更合适?
维度 | didFinishLaunching… | RunLoop 首次 idle |
---|---|---|
用户可见性 | UI 尚未出现 | 首帧已提交,布局/动画基本结束 |
度量一致性 | 受初始化代码多少影响大 | 对业务初始化代码鲁棒;只要主线程不被长任务阻塞,就在首个空闲循环采样 |
实现复杂度 | 最简单 | 略复杂,需要 CFRunLoopObserver |
与 Apple 报表对齐 | ❶ 对齐 MXAppLaunchMetric | 需要自行上报,与官方报表有 10‑50 ms 左右偏移 |
RunLoop idle 发生点:
kCFRunLoopBeforeWaiting
——系统即将把主线程睡眠,表明当前循环所有 Timer、Source、Layout、展示任务都完成了,首次出现通常紧跟在根 VC 的viewDidAppear:
之后citeturn13view0。
实战埋点模板(一次性)
/// 在 didFinishLaunching… 末尾安装一次观察者
static void InstallFirstIdleObserver(void) {CFRunLoopRef rl = CFRunLoopGetMain();static CFRunLoopObserverRef sObs;CFOptionFlags activities = kCFRunLoopBeforeWaiting; // 首次 idlesObs = CFRunLoopObserverCreateWithHandler(NULL, activities, /*repeat*/false, 0, ^(CFRunLoopObserverRef obs, CFRunLoopActivity act) {uint64_t t = mach_absolute_time() - g_appStartTime;[APMLogger recordColdLaunchIdle:t]; // 自定义上报CFRunLoopRemoveObserver(rl, obs, kCFRunLoopDefaultMode);CFRelease(obs);});CFRunLoopAddObserver(rl, sObs, kCFRunLoopDefaultMode);
}
要点
g_appStartTime
在main()
里用mach_absolute_time()
采集。- 只监听一次;如果用户在冷启动流程中立即进入后台可在回前台时重新安装。
- 避免在
didFinishLaunching…
里做重量级同步任务,否则 idle 会被推迟。
何时仍可选 didFinishLaunching…
?
- 无需精细区分 冷 / 预热 / 热启动,只想和
MetricKit
或 Firebase Perf 默认数据保持同一口径。 - 业务里真正耗时的初始化都放在
application(_:didFinishLaunching…)
之前(+load
、static
初始化或主函数),后续 UI 几乎“秒绘”。
TL;DR
“RunLoop 首次空闲” 把 UI 绘制、布局、动画都算进启动耗时,比
didFinishLaunchingWithOptions:
更贴近用户实际体验,且仍保持实现简洁可维护,是多数 APM 场景更合理的 启动结束 定义。
完整代码
下面给出一套 Objective‑C 冷启动监控模板,满足两点要求:
- 在
application:didFinishLaunching…
末尾安装一次 RunLoop idle 观察者 - 若用户在冷启动阶段马上切后台,再次回前台时自动重装观察者
⚙️ 如需 Swift 版本,可把 C‑API 调用 (
CFRunLoopObserver…
) 直接放到 Swift AppDelegate 中,逻辑完全一致。
// AppDelegate.m
#import "AppDelegate.h"
#import <mach/mach_time.h>// 记录进程启动瞬间:在 main() 里做
uint64_t gAppStartTime = 0;
__attribute__((constructor))
static void markProcessStart(void) {gAppStartTime = mach_absolute_time();
}/* ------------ 冷启动 RunLoop idle 监控核心 ------------ */@interface AppDelegate ()
@end@implementation AppDelegate {CFRunLoopObserverRef _idleObserver; // 当前安装的观察者BOOL _coldLaunchDone; // 是否已采集完成
}#pragma mark - Observer 安装 / 卸载- (void)installIdleObserverIfNeeded {if (_coldLaunchDone || _idleObserver) return; // 已完成或已安装CFRunLoopRef rl = CFRunLoopGetMain();_idleObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopBeforeWaiting, // RunLoop 即将 idle/*repeats*/ false, // 只触发一次0, // order^(CFRunLoopObserverRef obs, CFRunLoopActivity act) {uint64_t elapsed = mach_absolute_time() - gAppStartTime;[self reportColdLaunch:elapsed]; // ← 你的上报逻辑_coldLaunchDone = YES;CFRunLoopRemoveObserver(rl, obs, kCFRunLoopDefaultMode);CFRelease(obs);_idleObserver = NULL;});CFRunLoopAddObserver(rl, _idleObserver, kCFRunLoopDefaultMode);
}- (void)cancelIdleObserverIfNeeded {if (_idleObserver) {CFRunLoopRemoveObserver(CFRunLoopGetMain(), _idleObserver, kCFRunLoopDefaultMode);CFRelease(_idleObserver);_idleObserver = NULL;}
}#pragma mark - UIApplicationDelegate- (BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions {// …你的初始化代码…// 1️⃣ 仅在冷启动安装一次观察者[self installIdleObserverIfNeeded];[self observeAppLifeCycle]; // 订阅前后台事件return YES;
}#pragma mark - 前后台切换处理- (void)observeAppLifeCycle {NSNotificationCenter *nc = NSNotificationCenter.defaultCenter;[nc addObserver:self selector:@selector(_didEnterBackground)name:UIApplicationDidEnterBackgroundNotification object:nil];[nc addObserver:self selector:@selector(_willEnterForeground)name:UIApplicationWillEnterForegroundNotification object:nil];
}- (void)_didEnterBackground {// 2️⃣ 若尚未结束冷启动,取消当前观察者if (!_coldLaunchDone) {[self cancelIdleObserverIfNeeded];}
}- (void)_willEnterForeground {// 2️⃣ 回前台时,若冷启动仍未完成 → 重新安装观察者[self installIdleObserverIfNeeded];
}#pragma mark - 上报- (void)reportColdLaunch:(uint64_t)elapsedMach {// 将 mach 时间转换为毫秒mach_timebase_info_data_t info;mach_timebase_info(&info);double ms = (double)elapsedMach * info.numer / info.denom / 1e6;NSLog(@"[APM] Cold launch Time‑to‑Idle = %.1f ms", ms);// 调用你自己的 APM / 埋点上报接口……
}@end
关键点说明
位置 | 目的 |
---|---|
installIdleObserverIfNeeded | 在主 RunLoop 进入第一次 idle (kCFRunLoopBeforeWaiting ) 时触发;只安装一次,防止重复统计。 |
后台切换 | 如果在冷启动尚未结束时收到 DidEnterBackground ,先移除观察者;回到前台的 WillEnterForeground 再重新安装,保证最终一定能命中“首次 idle”。 |
_coldLaunchDone | 成功记录后置为 YES ,后续不再重复安装。 |
mach_absolute_time → 毫秒 | 使用 mach_timebase_info 做单位换算,避免因 CPU 频率变化带来的误差。 |
如需 Swift 写法,可用
RunLoop.main.add(_:forMode:)
加CFRunLoopObserverCreateWithHandler
的桥接版本,或直接使用CFRunLoopObserverCreate
结合Unmanaged<AnyObject>.fromOpaque
保存 self;逻辑保持一致即可。