iOS App 启动时间测量

当我们的 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 函数开始执行到 AppDelegateapplicationDidBecomeActive: 回调方法执行(App 被激活)的时间间隔,这个时间包含了的 App 启动时各初始化项的执行时间(一般写在 application:didFinishLaunchingWithOptions: 方法里),同时包含首页 UI 被渲染并显示出来的耗时。

Loading Time

对于第二个时间 Loading Time,比较好测量,我们可以在 main 函数开始执行和 applicationDidBecomeActive: 方法执行末尾时分别记录一个时间点,然后计算两者时间差即可,大致如下:

其中,关于 StartupTimeMonitor 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
// StartupTimeMonitor.h

#import <Foundation/Foundation.h>

@interface StartupTimeMonitor : NSObject

+ (instancetype)sharedMonitor;

- (void)appWillStartLoading;
- (void)appDidFinishLoading;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// StartupTimeMonitor.m

#import "StartupTimeMonitor.h"

@interface StartupTimeMonitor () {
CFAbsoluteTime _startTime;
CFAbsoluteTime _stopTime;
}

@end

@implementation StartupTimeMonitor

+ (instancetype)sharedMonitor {
static StartupTimeMonitor *sharedMonitor = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMonitor = [[StartupTimeMonitor alloc] init];
});
return sharedMonitor;
}

- (void)appWillStartLoading {
_startTime = CFAbsoluteTimeGetCurrent();
}

- (void)appDidFinishLoading {
_stopTime = CFAbsoluteTimeGetCurrent();

NSUInteger milliseconds = (NSUInteger)((_stopTime - _startTime) * 1000);
NSLog(@"Loading done in %lu ms", milliseconds);
}

@end

或者也可以这么写,在 main.m 中:

1
2
3
4
5
6
7
8
9
10
11
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

extern CFAbsoluteTime StartTime;

int main(int argc, char * argv[]) {
StartTime = CFAbsoluteTimeGetCurrent();
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

然后在 AppDelegate.m 中:

1
2
3
4
5
6
7
8
9
10
// Other code

CFAbsoluteTime StartTime;

- (void)applicationDidBecomeActive:(UIApplication *)application {
dispatch_async(dispatch_get_main_queue(), ^{
NSUInteger milliseconds = (NSUInteger)((CFAbsoluteTimeGetCurrent() - StartTime) * 1000);
NSLog(@"Loading done in %lu ms", milliseconds);
});
}

对于上述为何通过 dispatch_async(dispatch_get_main_queue(), ... 显式声明在主线程执行,可参考 StackOverflow 的这个帖子的讨论。

Pre-main Time

而对于第一个时间 Pre-main Time,目前没有比较好的人工测量手段,好在 Xcode 自身提供了一个在控制台打印这些时间的方法:在 Xcode 中 Edit Scheme -> Run -> Auguments 添加环境变量 DYLD_PRINT_STATISTICS 并把其值设为 1,如下图:

这样我们就可以在编译运行工程时,在控制台看到 Total pre-main time 总耗时了,如下图所示,包含 main 函数执行之前各项的加载时间,我们可以多次运行取一下平均值,苹果推荐这个时间应在 400ms 以内。

综上两步,我们就可计算出一个 iOS App 的启动耗时,并针对性进行优化。

不过,有一个比较滑稽的问题是:目前很多 App 都会在启动后加载一个 3~5 秒的广告页面,给用户的主观感受是这个 App 的启动时间包括了这个广告页的显示时间,于是我们在代码维度做的 App 启动时间优化显得似乎好无意义,sad…

参考链接