当我们的 App 大到一定规模时,就需要开始关注应用的启动时间了,因为这关系到用户体验问题。
我们通常说的启动时间为:用户点击应用图标,显示闪屏页,到该应用首页界面被加载出来的总时间(冷启动),对于 iOS App 来说,启动时间包括两部分:Launch Time = Pre-main Time + Loading Time,如下图所示,其中:
Pre-main Time
指 main 函数执行之前的加载时间,包括 dylib 动态库加载,Mach-O 文件加载,Rebase/Binding,Objective-C Runtime 加载等;Loading Time
指 main 函数开始执行到AppDelegate
的applicationDidBecomeActive:
回调方法执行(App 被激活)的时间间隔,这个时间包含了的 App 启动时各初始化项的执行时间(一般写在application:didFinishLaunchingWithOptions:
方法里),同时包含首页 UI 被渲染并显示出来的耗时。
Loading Time
对于第二个时间 Loading Time,比较好测量,我们可以在 main 函数开始执行和 applicationDidBecomeActive: 方法执行末尾时分别记录一个时间点,然后计算两者时间差即可,大致如下:
//在main函数加上 [[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 添加环境变量 DYLD_PRINT_STATISTICS
并把其值设为 1
,如下图:
这样我们就可以在编译运行工程时,在控制台看到 Total pre-main time 总耗时了
页面加载时间
如果想统计每个页面的加载时间,我的处理方式是HOOK每个控制器的loadView 方法 和 viewDidAppear方法 在这两个方法中分别记录时间,每个页面的加载时间就是viewDidAppear的加载时间减去loadView里的记录时间
注:这里推荐一个好用的hook库 Aspects
这里贴出普通页面的时间统计 如果我们想统计每个页面的加载时间 我们需要有一个基类,所有的控制器都继承于这个基类
pragma mark 普通页面使用统计
- (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) {
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) {
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:--- %@ --- 关闭",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) {
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:--- %@ --- 关闭",clazz);
NSDate *date = classInfo[@"classUseDate"];
[weakSelf.normalVCUse removeObject:classInfo];
if (date ) {
NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:date];
/*
当使用过程中程序进入后台并停留一段时间时,统计时长需要减去该段时间
*/
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 中加入以下代码
// 初始化并配置统计工具
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;