結論先行
在這兩個候選時間點里——
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;邏輯保持一致即可。