0. 概念
1.热启动:就是按下home键的时候,app还存在一段时间,这时点击app马上就能恢复到原状态,这种启动我们称为热启动。
2.冷启动:App被kill掉之后,重新打开启动过程为冷启动。
1.热启动优化。
一.数据优化,将耗时操作做异步处理。
二.检查NSUserDefaults的存储,NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,加载的时候是整个plist配置文件全部load到内存中。所以非常频繁的存取大量数据也是有可能导致APP启动卡顿的, 本文重点讲讲冷启动的优化
1. App启动过程
1.1解析Info.plist
- 加载相关信息,例如如闪屏
- 沙箱建立、权限检查
1.2Mach-O加载
- 如果是胖二进制文件,寻找合适当前CPU类别的部分
- 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
- 定位内部、外部指针引用,例如字符串、函数等
- 执行声明为attribute((constructor))的C函数
- 加载类扩展(Category)中的方法
- C++静态对象加载、调用ObjC的 +load 函数
1.3程序执行
- 调用main()
- 调用UIApplicationMain()
- 调用applicationWillFinishLaunching
2. 如何测量启动过程耗时
2.1 main()函数之前
在不越狱的情况下,以往很难精确的测量在main()函数之前的启动耗时,因而我们也往往容易忽略掉这部分数据。小型App确实不需要太过关注这部分。但如果是大型App(自定义的动态库超过50个、或编译结果二进制文件超过30MB),这部分耗时将会变得突出。所幸,苹果已经在Xcode中加入这部分的支持。
在Xcode的菜单中选择Project→Scheme→Edit Scheme...,然后找到 Run → Environment Variables →+,添加name为DYLD_PRINT_STATISTICS的value为1的环境变量。
在Xcode运行App时,会在console中会得到这样一个报告:
Total pre-main time: 94.33 milliseconds (100.0%)
dylib loading time: 61.87 milliseconds (65.5%)
rebase/binding time: 3.09 milliseconds (3.2%)
ObjC setup time: 10.78 milliseconds (11.4%)
initializer time: 18.50 milliseconds (19.6%)
slowest intializers :
libSystem.B.dylib : 3.59 milliseconds (3.8%)
libBacktraceRecording.dylib : 3.65 milliseconds (3.8%)
GTFreeWifi : 7.09 milliseconds (7.5%)
如何解读
- main()函数之前总共使用了94.33ms
- 在94.33ms中,加载动态库用了61.87ms,指针重定位使用了3.09ms,ObjC类初始化使用了10.78ms,各种初始化使用了18.50ms。
- 在初始化耗费的18.50ms中,用时最多的三个初始化是libSystem.B.dylib、libBacktraceRecording.dylib以及GTFreeWifi。
2.1main()函数之后
从main()函数开始至applicationWillFinishLaunching结束,我们统一称为main()函数之后的部分。
3. 影响启动性能的因素
App启动过程中每一个步骤都会影响启动性能,但是有些部分所消耗的时间少之又少,另外有些部分根本无法避免,考虑到投入产出比,我们只列出我们可以优化的部分:
3.1 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()来完成的,就尽量不要用到以上的方法。
3.2main()函数之后耗时的影响因素
- 执行main()函数的耗时
- 执行applicationWillFinishLaunching的耗时
- rootViewController及其childViewController的加载、view及其subviews的加载
applicationWillFinishLaunching的耗时
如果有这样这样的代码:
//AppDelegate.m
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.rootViewController = [[[MQQTabBarController alloc] init] autorelease];
self.window = [[[UIWindow alloc] init] autorelease];
[self.window makeKeyAndVisible];
self.window.rootViewController = self.rootViewController;
UITabBarController *tabBarViewController = [[[UITabBarController alloc] init] autorelease];
NSLog(@"%s", __PRETTY_FUNCTION__);
return YES;
}
...
//MQQTabBarController.m
@implementation MQQTabBarController
- (void)viewDidLoad {
NSLog(@"%s", __PRETTY_FUNCTION__);
[super viewDidLoad];
// Do any additional setup after loading the view.
UIViewController *tab1 = [[[MQQTab1ViewController alloc] init] autorelease];
tab1.tabBarItem.title = @"red";
[self addChildViewController:tab1];
UIViewController *tab2 = [[[MQQTab2ViewController alloc] init] autorelease];
tab2.tabBarItem.title = @"blue";
[self addChildViewController:tab2];
UIViewController *tab3 = [[[MQQTab3ViewController alloc] init] autorelease];
tab3.tabBarItem.title = @"green";
[self addChildViewController:tab3];
}
...
@end
执行的先后顺序是:
- -[MQQTabBarController viewDidLoad]
- -[MQQTab1ViewController viewDidLoad]
- -[AppDelegate application:didFinishLaunchingWithOptions:]
- -[MQQTab2ViewController viewDidLoad] (点击了第二个tab之后加载)
5.-[MQQTab3ViewController viewDidLoad] (点击了第三个tab之后加载)
一般而言,大部分情况下我们都会把界面的初始化过程放在viewDidLoad,但是这个过程会影响消耗启动的时间。特别是在类似TabBarController这种会嵌套childViewController的ViewController的情况,它也会把部分children也初始化,因此各种viewDidLoad会递归的进行。
最简单的解决的方法,是把viewController延后加载,但实际上这属于一种掩耳盗铃,确实,applicationWillFinishLaunching的耗时是降下来了,但用户体验上并没有感觉变快。
更好一点的解决方法有点类似facebook,主视图会第一时间加载,但里面的数据和界面都会延后加载,这样用户就会阶段性的获得视觉上的变化,从而在视觉体验上感觉App启动得很快。
4.优化的目标
由于每个App的情况有所不同,需要加载的数据量也有所不同,事实上我们无法使用一种统一的标准来衡量不同的App。苹果。
应该在400ms内完成main()函数之前的加载
整体过程耗时不能超过20秒,否则系统会kill掉进程,App启动失败
400ms内完成main()函数前的加载的建议值是怎样定出来的呢?其实我也没有太深究过这个问题,但是,当用户点击了一个App的图标时,iOS做动画到闪屏图出现的时长正好是这个数字,我想也许跟这个有关。
针对不同规模的App,我们的目标应该有所取舍。例如,对于像手机QQ这种集整个SNG的代码大成撸出来的App,对动态库的使用在所难免,但对于WiFi管家,由于在用户连接WiFi的时候需要非常快速的响应,所以快速启动就非常重要。
那么,如何定制优化的目标呢?首先,要确定启动性能的界限,例如,在各种App性能的指标中,哪一此属于启动性能的范畴,哪一些则于App的流畅度性能?我认为应该首先把启动过程分为四个部分:
main()函数之前
main()函数之后至applicationWillFinishLaunching完成
App完成所有本地数据的加载并将相应的信息展示给用户
App完成所有联网数据的加载并将相应的信息展示给用户
1+2一起决定了我们需要用户等待多久才能出现一个主视图,同时也是技术上可以精确测量的时长,1+2+3决定了用户视觉上的等待出现有用信息所需要的时长,1+2+3+4决定了我们需要多少时间才能让我们需要展示给用户的所有信息全部出现。
淘宝的iOS客户端无疑是各部分都做得非常优秀的典型。它所承载的业务完全不比微信和手机QQ少,但几乎瞬间完成了启动,并利用缓存机制使得用户马上看到“貌似完整”的界面,然后立即又刷新了刚刚联网更新回来的信息。也就是说,无论是技术上还是视觉上,它都非常的“快”。
5. 具体措施
- 移除不需要用到的动态库
- 移除不需要用到的类
- 合并功能类似的类和扩展(Category)
- 压缩资源图片
- 优化applicationWillFinishLaunching
- 优化rootViewController加载
- 挖掘最后一点性能优化
总结
6.1利用DYLD_PRINT_STATISTICS分析main()函数之前的耗时
- 重新梳理架构,减少动态库、ObjC类的数目,减少Category的数目
- 定期扫描不再使用的动态库、类、函数,例如每两个迭代一次
- 用dispatchonce()代替所有的attribute((constructor))函数、C++静态对象初始化、ObjC的+load
- 在设计师可接受的范围内压缩图片的大小,会有意外收获
6.2利用锚点分析applicationWillFinishLaunching的耗时
- 将不需要马上在applicationWillFinishLaunching执行的代码延后执行
- rootViewController的加载,适当将某一级的childViewController或subviews延后加载
- 如果你的App可能会被后台拉起并冷启动,可考虑不加载rootViewController
6.3不应放过的一些小细节
- 异步操作并不影响指标,但有可能影响交互体验,例如大量网络请求导致数据拥堵
- 有时候一些交互上的优化比技术手段效果更明显,视觉上的快决不是冰冷的数据可以解释的,好好和你们的设计师谈谈动画
抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%
https://blog.csdn.net/wangletiancsdn/article/details/103597604
针对于今日头条这个App我们可以优化的点如下:
纯代码方式而不是storyboard加载首页UI。
对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载。
对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。