iOS App 的启动性能(一)
介绍一下如何优化 iOS App 的启动性能,分为四个部分:
第一部分科普了一些和App启动性能相关的前置知识
第二部分主要讲如何定制启动性能的优化目标
第三部分通过在WiFi管家这个具体项目的优化过程,分享一些有用的经验
第四部分是关键点的总结。
【第一部分】一些小科普
因为篇幅的限制,没有办法很详尽的说明一些原理性的东西,只是方便大家了解哪些事情可能跟启动性能有关。同时,内容相对也比较入门,大神们请跳过这一部分。
1. App启动过程
解析Info.plist
加载相关信息,例如如闪屏
沙箱建立、权限检查
Mach-O加载
如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为__attribute__((constructor))的C函数
加载类扩展(Category)中的方法
C++静态对象加载、调用ObjC的+load函数
程序执行
调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching
2. 如何测量启动过程耗时
冷启动比热启动重要
当用户按下home键的时候,iOS的App并不会马上被kill掉,还会继续存活若干时间。理想情况下,用户点击App的图标再次回来的时候,App几乎不需要做什么,就可以还原到退出前的状态,继续为用户服务。这种持续存活的情况下启动App,我们称为热启动,相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。我们这里只讨论App冷启动的情况。
main()函数之前
在不越狱的情况下,以往很难精确的测量在main()函数之前的启动耗时,因而我们也往往容易忽略掉这部分数据。小型App确实不需要太过关注这部分。但如果是大型App(自定义的动态库超过50个、或编译结果二进制文件超过30MB),这部分耗时将会变得突出。所幸,苹果已经在Xcode中加入这部分的支持。
苹果提供的方法
在Xcode的菜单中选择Project→Scheme→Edit Scheme...,然后找到Run→Environment Variables→+,添加name为DYLD_PRINT_STATISTICSvalue为1的环境变量。
在Xcode运行App时,会在console中得到一个报告。例如,我在WiFi管家中加入以上设置之后,会得到这样一个报告:
Total pre-main time: 94.33milliseconds (100.0%)
dylib loading time: 61.87milliseconds (65.5%)
rebase/binding time: 3.09milliseconds (3.2%)
ObjC setup time: 10.78milliseconds (11.4%)
initializer time: 18.50milliseconds (19.6%)
slowest intializers :
libSystem.B.dylib: 3.59milliseconds (3.8%)
libBacktraceRecording.dylib: 3.65milliseconds (3.8%)
GTFreeWifi : 7.09milliseconds (7.5%)
如何解读
main()函数之前总共使用了94.33ms
在94.33ms中,加载动态库用了61.87ms,指针重定位使用了3.09ms,ObjC类初始化使用了10.78ms,各种初始化使用了18.50ms。
在初始化耗费的18.50ms中,用时最多的三个初始化是libSystem.B.dylib、libBacktraceRecording.dylib以及GTFreeWifi。
main()函数之后
从main()函数开始至applicationWillFinishLaunching结束,我们统一称为main()函数之后的部分。
3. 影响启动性能的因素
App启动过程中每一个步骤都会影响启动性能,但是有些部分所消耗的时间少之又少,另外有些部分根本无法避免,考虑到投入产出比,我们只列出我们可以优化的部分:
main()函数之前耗时的影响因素
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load越多,启动越慢
实验证明,在ObjC类的数目一样多的情况下,需要加载的动态库越多,App启动就越慢。同样的,在动态库一样多的情况下,ObjC的类越多,App的启动也越慢。需要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个上升到100个的时候就会变得十分明显。同理,100个类和1000个类,可能也很难查察觉得出,但1000个类和10000个类的分别就开始明显起来。
同样的,尽量不要写__attribute__((constructor))的C函数,也尽量不要用到C++的静态对象;至于ObjC的+load方法,似乎大家已经习惯不用它了。任何情况下,能用dispatch_once()来完成的,就尽量不要用到以上的方法。
main()函数之后耗时的影响因素
执行main()函数的耗时
执行applicationWillFinishLaunching的耗时
rootViewController及其childViewController的加载、view及其subviews的加载
applicationWillFinishLaunching的耗时
如果有这样这样的代码:
//AppDelegate.m
@implementationAppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
self.rootViewController= [[[MQQTabBarController alloc] init] autorelease];
self.window= [[[UIWindowalloc] init] autorelease];
[self.windowmakeKeyAndVisible];
self.window.rootViewController=self.rootViewController;
UITabBarController*tabBarViewController = [[[UITabBarControlleralloc] init] autorelease];
NSLog(@"%s", __PRETTY_FUNCTION__);
returnYES;
}
...
//MQQTabBarController.m
@implementationMQQTabBarController
- (void)viewDidLoad {
NSLog(@"%s", __PRETTY_FUNCTION__);
[superviewDidLoad];
// Do any additional setup after loading the view.
UIViewController*tab1 = [[[MQQTab1ViewController alloc] init] autorelease];
tab1.tabBarItem.title=@"red";
[selfaddChildViewController:tab1];
UIViewController*tab2 = [[[MQQTab2ViewController alloc] init] autorelease];
tab2.tabBarItem.title=@"blue";
[selfaddChildViewController:tab2];
UIViewController*tab3 = [[[MQQTab3ViewController alloc] init] autorelease];
tab3.tabBarItem.title=@"green";
[selfaddChildViewController:tab3];
}
...
@end
那么-[MQQTabBarController viewDidLoad]、-[AppDelegate application:didFinishLaunchingWithOptions:]、-[MQQTab1ViewController viewDidLoad]、-[MQQTab2ViewController viewDidLoad]、-[MQQTab2ViewController viewDidLoad]完成的先后顺序是怎样的呢?
答案是:
-[MQQTabBarController viewDidLoad]
-[MQQTab1ViewController viewDidLoad]
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[MQQTab2ViewController viewDidLoad](点击了第二个tab之后加载)
-[MQQTab3ViewController viewDidLoad](点击了第三个tab之后加载)
一般而言,大部分情况下我们都会把界面的初始化过程放在viewDidLoad,但是这个过程会影响消耗启动的时间。特别是在类似TabBarController这种会嵌套childViewController的ViewController的情况,它也会把部分children也初始化,因此各种viewDidLoad会递归的进行。
最简单的解决的方法,是把viewController延后加载,但实际上这属于一种掩耳盗铃,确实,applicationWillFinishLaunching的耗时是降下来了,但用户体验上并没有感觉变快。
更好一点的解决方法有点类似facebook,主视图会第一时间加载,但里面的数据和界面都会延后加载,这样用户就会阶段性的获得视觉上的变化,从而在视觉体验上感觉App启动得很快。
如果有技术交流可以加我V:herodon(也可以交流一下最近学习的 马士兵 极客go 码牛)