當(dāng)我們的 App 大到一定規(guī)模時,就需要開始關(guān)注應(yīng)用的啟動時間了,因?yàn)檫@關(guān)系到用戶體驗(yàn)問題。
我們通常說的啟動時間為:用戶點(diǎn)擊應(yīng)用圖標(biāo)蛮放,顯示閃屏頁,到該應(yīng)用首頁界面被加載出來的總時間(冷啟動)奠宜,對于 iOS App 來說包颁,啟動時間包括兩部分:Launch Time = Pre-main Time + Loading Time,如下圖所示压真,其中:
Pre-main Time
指 main 函數(shù)執(zhí)行之前的加載時間娩嚼,包括 dylib 動態(tài)庫加載,Mach-O 文件加載榴都,Rebase/Binding待锈,Objective-C Runtime 加載等;Loading Time
指 main 函數(shù)開始執(zhí)行到AppDelegate
的applicationDidBecomeActive:
回調(diào)方法執(zhí)行(App 被激活)的時間間隔嘴高,這個時間包含了的 App 啟動時各初始化項(xiàng)的執(zhí)行時間(一般寫在application:didFinishLaunchingWithOptions:
方法里)竿音,同時包含首頁 UI 被渲染并顯示出來的耗時和屎。
Loading Time
對于第二個時間 Loading Time,比較好測量春瞬,我們可以在 main 函數(shù)開始執(zhí)行和 applicationDidBecomeActive: 方法執(zhí)行末尾時分別記錄一個時間點(diǎn)柴信,然后計(jì)算兩者時間差即可,大致如下:
//在main函數(shù)加上 [[XYYAPMLoadMonitor shareManager]startAPPOpenTime];
code10以及以下
//在AppDelegate didFinishLaunchingWithOptions的第一行 中加入 [[XYYAPMLoadMonitor shareManager]appInitTime];
//在AppDelegate didFinishLaunchingWithOptions的最后一行 中加入 [[XYYAPPStartUpMonitorManager shareManager]firstVCLoadDoneTime];
Xcode11(生命周期交給UIWindowScene來管理)
需要在- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions
方法里 相同位置注入上面的代碼
Pre-main Time
而對于第一個時間 Pre-main Time宽气,目前沒有比較好的人工測量手段随常,好在 Xcode 自身提供了一個在控制臺打印這些時間的方法:在 Xcode 中 Edit Scheme -> Run -> Auguments 添加環(huán)境變量 DYLD_PRINT_STATISTICS
并把其值設(shè)為 1
,如下圖:
這樣我們就可以在編譯運(yùn)行工程時萄涯,在控制臺看到 Total pre-main time 總耗時了
頁面加載時間
如果想統(tǒng)計(jì)每個頁面的加載時間绪氛,我的處理方式是HOOK每個控制器的loadView 方法 和 viewDidAppear方法 在這兩個方法中分別記錄時間,每個頁面的加載時間就是viewDidAppear的加載時間減去loadView里的記錄時間
注:這里推薦一個好用的hook庫 Aspects
這里貼出普通頁面的時間統(tǒng)計(jì) 如果我們想統(tǒng)計(jì)每個頁面的加載時間 我們需要有一個基類涝影,所有的控制器都繼承于這個基類
pragma mark 普通頁面使用統(tǒng)計(jì)
- (void)pagesUsingStatisticWithArray:(NSArray *)array{
__block __weak typeof(self) weakSelf = self;
// screen views tracking
for (NSDictionary *trackedScreen in array) {
Class clazz = NSClassFromString(trackedScreen[@"className"]);
//頁面開始加載時間
[clazz aspect_hookSelector:@selector(loadView)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSString *className = NSStringFromClass([aspectInfo.instance class]);
if (weakSelf.openLog){
//NSLog(@"aspectInfo:%@",NSStringFromClass([aspectInfo.instance class]));
NSLog(@"className:--- %@ --- 頁面開始加載",className);
}
NSDictionary *classInfo = @{@"className":className,
@"pageName":trackedScreen[@"pageName"],
@"pageStartDate":[NSDate date]};
[weakSelf.pageStartTimes addObject:classInfo];
});
}
error:nil];
[clazz aspect_hookSelector:@selector(viewDidAppear:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSArray *pageDrutions = [NSArray arrayWithArray:weakSelf.pageStartTimes] ;
for (NSDictionary *classInfo in pageDrutions) {
NSString *className = NSStringFromClass([aspectInfo.instance class]);
if ([classInfo[@"className"] isEqualToString:className]) {
// NSLog(@"className:--- %@ --- 關(guān)閉",clazz);
NSDate *date = classInfo[@"pageStartDate"];
//long dateTimeInterVal = [weakSelf getDateTimeTOMilliSeconds:date];
[weakSelf.pageStartTimes removeObject:classInfo];
if (date ) {
// long long currentTimeInterVal = [weakSelf getDateTimeTOMilliSeconds:[NSDate date]];
// long long duration = currentTimeInterVal - dateTimeInterVal;
NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:date]*1000;
if (weakSelf.openLog){
NSLog(@"className:--- %@ --- 頁面啟動時間 pageStartDrutionDate: %f豪秒",className,duration);
}
[weakSelf markStatisticLogWithLogName:className Duration:duration];
}
}
}
NSString *className = NSStringFromClass([aspectInfo.instance class]);
if (weakSelf.openLog){
//NSLog(@"aspectInfo:%@",NSStringFromClass([aspectInfo.instance class]));
NSLog(@"className:--- %@ --- 頁面開始完成",className);
}
NSDictionary *classInfo = @{@"className":className,
@"pageName":trackedScreen[@"pageName"],
@"classUseDate":[NSDate date]};
[weakSelf.normalVCUse addObject:classInfo];
});
}
error:nil];
SEL selektor = NSSelectorFromString(@"viewDidDisappear:");
[clazz aspect_hookSelector:selektor
withOptions:AspectPositionBefore
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
NSArray *normalVCDrutions = [NSArray arrayWithArray:weakSelf.normalVCUse];
for (NSDictionary *classInfo in normalVCDrutions) {
Class cls = [aspectInfo.instance class];
NSString *className = NSStringFromClass(cls);
if ([classInfo[@"className"] isEqualToString:className]) {
// NSLog(@"className:--- %@ --- 關(guān)閉",clazz);
NSDate *date = classInfo[@"classUseDate"];
[weakSelf.normalVCUse removeObject:classInfo];
if (date ) {
NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:date];
/*
當(dāng)使用過程中程序進(jìn)入后臺并停留一段時間時枣察,統(tǒng)計(jì)時長需要減去該段時間
*/
if (self.backDate && ([self.backDate timeIntervalSinceDate:date] > 0)) {
duration = duration - [self.aliveDate timeIntervalSinceDate:self.backDate];
}
if (weakSelf.openLog){
NSLog(@"className:--- %@ --- 用戶使用并停留 useTimeDuration: %.2f秒",className,duration);
}
[weakSelf markStatisticLogWithLogName:className Duration:duration];
}
}
}
});
}
error:nil];
}
}
//在AppDelegate didFinishLaunchingWithOptions 中加入以下代碼
// 初始化并配置統(tǒng)計(jì)工具
XYYAPMLoadMonitor *manager = [XYYAPMLoadMonitor shareManager];
[manager setupWithTabbarControllerNames:@[@"XYYHomeViewController",@"XYYAllDrugsViewController",@"XYYFoundViewController",@"XYYShoppingCartViewController",@"XYYMeViewController"] controllers:@[@"XYYBaseController"] appDelegate:@"XYYAppDelegate"];
[manager setupWithTabbarControllerNames:@[] controllers:@[@"XYYBaseController"]];
manager.logStrategy = XYYLogSendStrategyCustom;
manager.enableExceptionLog = NO;
manager.logSendInterval = 1;
manager.openLog = NO;
manager.enableMonitor = NO;