iOS启动优化:App启动耗时在线监控与AppDelegate管
一、App启动耗时在线监控
在大型的多团队合作的项目中,往往不经意间的一个个改动,可能就会直接或累加式的拖慢App的启动速度,测试人员通过本地的录屏或者开发工具测量启动耗时由于受测试机器的状态和样本数量的原因数据往往有波动,并不能真正反馈App启动时间的真实变化。所以加入在线的数据监控变得非常重要。
在介绍App启动耗时监控之前,我们先大概回顾下App启动过程:
启动过程main()
函数之前的阶段我们成为pre-main()
,至于pre-main()
中每个阶段的具体作用,这边就不再赘述,网上资料较多。
pre-main()开始时间:__t1
苹果并没有直接提供App启动的开始时间,目前业内主要有两种标准作为App的启动时间:
-
第一个
+(void)load
被调用时我们知道
+(void)load
方法调用发生Initializer
阶段, 根据动态库的加载顺序调用+(void)load
方法,而动态库的加载顺序是递归加载的,我们只要找到叶子节点的动态库,然后在这个动态库中的添加+(void)load
方法来记录启动时间。很明显,这种方式没有统计到Initializer
前面的时间,比如增加动态库,Category等造成的启动耗时并不能被及时发现。 -
获取进程创建时间
我们的App实际上是一个进程,如果能获取到进程的创建时间,也就是
exec()
阶段的时间,更能提前记录到App的启动开始时间。#import <sys/sysctl.h> #import <mach/mach.h> + (CFAbsoluteTime)processStartTime { if (__t1 == 0) { struct kinfo_proc procInfo; int pid = [[NSProcessInfo processInfo] processIdentifier]; int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; size_t size = sizeof(procInfo); if (sysctl(cmd, sizeof(cmd)/sizeof(*cmd), &procInfo, &size, NULL, 0) == 0) { __t1 = procInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + procInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0; } } return __t1; }
pre-main()结束时间:__t2
获取pre-main()
结束时间相对容易,可以main()函数的开始执行时间,而我更推荐使用__attribute__((constructor))
函数调用作为pre-main()
的结束时间,这样能最大程度的实现解耦:
void static __attribute__((constructor)) before_main() {
if (__t2 == 0) {
__t2 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
}
至于为什么不用最后一个load方法执行时间?因为在大型工程中我们没办法确定哪个load方法是最后一个...
启动完成时间:__t3
启动完成时间一般可以通过获取didFinishLaunchingWithOptions:
的结束时间,但是didFinishLaunchingWithOptions:
的结束时间其实不包括启动图动画的时间,启动图动画执行完成后的时间更接近用户的感官。我们可以在执行didFinishLaunchingWithOptions:
的runloop循环的后面的循环来获取时间:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//do somethings
dispatch_async(dispatch_get_main_queue(), ^{
if (__t3 == 0) {
__t3 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
});
return YES;
}
二、AppDelegate管控
在iOS项目中,AppDelegate
类里面的代码往往都是杂乱无章,各模块只要有需要,都会往AppDelegate
中添加各种各样的方法,特别是didFinishLaunchingWithOptions:
方法更是重灾区。为什么要把AppDelegate的管控和启动耗时监控写在一起呢?因为,合理且统一的模块调用方式,可以更好的去统计每个模块的调用耗时,进而实现更精细化的监控。
-
抽象统一的方法;
如需要在
didFinishLaunchingWithOptions:
被调用的模块都在自己的模块中实现+ (void)didFinishLaunchingWithOptions
的静态方法。 -
在
didFinishLaunchingWithOptions:
中使用rouer或者动态调用各模块的对应的静态方法。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //do somethings //... NSArray *modules = @[@"Module1",@"Module2",@"Module3",@"Module4",@"Module5"]; for (NSString *module in modules) { //1.获取开始时间 CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); //2.调用模块 NSString *url = [NSString stringWithFormat:@"%@://didFinishLaunchingWithOptions",module]; [Router openUrl:url]; //3.获取消耗的时间 CFAbsoluteTime cost = CFAbsoluteTimeGetCurrent() - start; //4.记录、上报 ... } return YES; }
-
将AppDelegate制作成静态库,如AppDelegate.framework。main函数中修改(非pods工程,记得在
Other Linker Flags
中添加-force_load
,避免链接时被优化):int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, @"AppDelegate")); } }
AppDelegate这样模块可以交由专人维护,进而实现了AppDelegate的管控。