1.App启动过程
- 解析Info.plist
- 加载相关信息,例如如闪屏
- 沙箱建立、权限检查
- Mach-O加载
- 如果是胖二进制文件,寻找合适当前CPU类别的部分
- 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
- 定位内部、外部指针引用,例如字符串、函数等
- 执行声明为attribute((constructor))的C函数
- 加载类扩展(Category)中的方法
- C++静态对象加载、调用ObjC的 +load 函数
- 程序执行
- 调用main()
- 调用UIApplicationMain()
- 调用applicationWillFinishLaunching
扩展
- Mach-O文件是什么:
- Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。
- 常见的Mach-O文件类型
- MH_OBJECT
目标文件(.o)
静态库文件(.a),静态库其实就是N个.o合并在一起
- MH_EXECUTE:可执行文件
.app/xx- MH_DYLIB:动态库文件
.dylib
.framework/xx
- MH_DYLINKER:动态链接编辑器
/usr/lib/dyld
- MH_DSYM:存储着二进制文件符号信息的文件
.dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
- 目标文件类型
- Mach-O 文件
Mach-O 文件包含一种架构(i386、x86_64、arm64 等等)的对象代码
- 通用二进制文件
也叫作胖文件,胖文件可能包含若干包含不同架构(i386、x86_64、arm、arm64 等等)对象代码的对象文件
毫无疑问移动应用的启动时间是影响用户体验的一个重要方面,那么我们究竟该如何通过启动时间来衡量一个应用性能的好坏呢?启动时间可以从冷启动和热启动两个角度去测量:
冷启动:指的是应用尚未运行,必须加载并构建整个应用,完成初始化的工作,冷启动往往比热启动耗时长,而且每个应用的冷启动耗时差别也很大,所以冷启动存在很大的优化空间,冷启动时间从applicationDidFinishLaunching:withOptions:方法开始计算,很多应用会在该方法对其使用的第三方库初始化。
热启动:应用已经在后台运行(常见的场景是用户按了 Home 按钮),由于某个事件将应用唤醒到前台,应用会在 applicationWillEnterForeground: 方法接收应用进入前台的事件
APP的启动时间,直接影响用户对你的APP的第一体验和判断。如果启动时间过长,不单单体验直线下降,而且可能会激发苹果的watch dog机制kill掉你的APP,那就悲剧了,用户会觉得APP怎么一启动就卡死然后崩溃了,不能用,然后长按APP点击删除键。(Xcode在debug模式下是没有开启watch dog的,所以我们一定要连接真机测试我们的APP)
t(App 总启动时间) = t1(
main()
之前的加载时间 ) + t2(main()
之后的加载时间 )。
- t1 = 系统的 dylib (动态链接库)和 App 可执行文件的加载时间;
- t2 =
main()
函数执行之后到AppDelegate
类中的applicationDidFinishLaunching:withOptions:
方法执行结束前这段时间。
所以我们对APP启动时间的获取和优化都是从这两个阶段着手,下面先看看main()
函数执行之前如何获取启动时间。
2.衡量main()函数执行之前的耗时
对于衡量main()之前也就是time1的耗时,苹果官方提供了一种方法,即在真机调试的时候,勾选DYLD_PRINT_STATISTICS
选项(如果想获取更详细的信息可以使用DYLD_PRINT_STATISTICS_DETAILS
),如下图:
输出结果如下:
Total pre-main time: 34.22 milliseconds (100.0%)
dylib loading time: 14.43 milliseconds (42.1%)
rebase/binding time: 1.82 milliseconds (5.3%)
ObjC setup time: 3.89 milliseconds (11.3%)
initializer time: 13.99 milliseconds (40.9%)
slowest intializers :
libSystem.B.dylib : 2.20 milliseconds (6.4%)
libBacktraceRecording.dylib : 2.90 milliseconds (8.4%)
libMainThreadChecker.dylib : 6.55 milliseconds (19.1%)
libswiftCoreImage.dylib : 0.71 milliseconds (2.0%)
系统级别的动态链接库,因为苹果做了优化,所以耗时并不多,而大多数时候,t1的时间大部分会消耗在我们自身App中的代码上和链接第三方库上。
**main()函数之前耗时的影响因素
- 动态库加载越多,启动越慢。
- ObjC类越多,启动越慢
- C的constructor函数越多,启动越慢
- C++静态对象越多,启动越慢
- ObjC的+load越多,启动越慢
所以我们应如何减少main()调用之前的耗时呢,我们可以优化的点有:
- 减少不必要的framework,特别是第三方的,因为动态链接比较耗时;
- check framework应设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
- 合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具:
- 删减一些无用的静态变量
- 删减没有被调用到或者已经废弃的方法
- 将不必须在+load方法中做的事情延迟到+initialize中
- 尽量不要用C++虚函数(创建虚函数表有开销)
3.衡量main()函数执行之后的耗时
第二阶段的耗时统计,我们认为是从main ()
执行之后到applicationDidFinishLaunching:withOptions:
方法最后,那么我们可以通过打点的方式进行统计。 Objective-C项目因为有main文件,所以我么直接可以通过添加代码获取:
// 1. 在 main.m 添加如下代码:
CFAbsoluteTime AppStartLaunchTime;
int main(int argc, char * argv[]) {
AppStartLaunchTime = CFAbsoluteTimeGetCurrent();
.....
}
// 2. 在 AppDelegate.m 的开头声明
extern CFAbsoluteTime AppStartLaunchTime;
// 3. 最后在AppDelegate.m 的 didFinishLaunchingWithOptions 中添加
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"App启动时间--%f",(CFAbsoluteTimeGetCurrent()-AppStartLaunchTime));
});
main函数之后的优化:
- 尽量使用纯代码编写,减少xib的使用;
- 启动阶段的网络请求,是否都放到异步请求;
- 一些耗时的操作是否可以放到后面去执行,或异步执行等。
4.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]
、 [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启动得很快。
5. facebook启动的网络请求优化
网络请求/响应看起来像这样:
我们注意到,一旦请求正在排队,发送请求出去之后就有一个时间间隔。这很好解释 — 在冷启动中,网络连接并不是一个开放的、安全的 TCP 连接。一个连接的建立需要三次握手,平均为几百毫秒。当摘要请求第一次发送时,无法避免要花掉这些时间。长远来看,这可以通过缓存 SSL 证书来解决。但是再次强调,我们退回来的目的并不是为了发送 TCP 请求,而是为了从服务器通过任何可能的方式获得请求信息。
我们提出了一个创造性的解决方案 — UDP 启动。本质上,我们在通过 TCP 发送摘要请求时,先发送一个编码过的包含摘要请求的 UDP 包到服务器。这样做的目的是唤醒服务器更早地去获取和缓存数据。当真正的摘要请求通过 TCP 到达时,服务器只需见到地从缓存内容中构造出响应,并发回客户端。这个技术使得我们可以减少几百毫秒的耗时。
当我们持续深入研究服务器端时,我们开始尝试使用 层-取(story-fetching)策略。过去我们已经做了一批摘要请求的 3+7 层。原因很简单:下载次数和被下载的层成正比。因此,把请求分割成两块,允许开始的三层先进来,其余的七个随后进来。通过提升我们的基础设施,我们已经能够升级为 1+1+X 策略,这已经接近于流了。这样就减少了服务器必须处理第一层的时间,并且能够减少下载的时间,使得可以在最快的时间内与用户交互。通过这样的努力,这样我们又减少了几百毫秒的耗时。
总结一下facebook网络优化就是:
- 瘦身请求网络依赖,将相类似的多个请求归到一个请求
- UDP启动请求现行缓存
- 队列串行化处理启动响应
6.优化总结
性能上的优化:
main()函数之前
- 减少动态库静态库等Mach-O文件的加载
- 合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具
- 尽量不要用C++虚函数(创建虚函数表有开销)
- 合并功能类似的类和扩展(Category)
- 压缩资源图片
main()函数之后
- 尽量使用纯代码编写,减少xib的使用
- 启动阶段的网络请求,是否都放到异步请求
- 一些耗时的操作放到后面去执行,或异步执行等
4.优化rootViewController加载,减少或延后加载不需要的视图及逻辑 - 网络请求的优化。。。
- 数据本地缓存,先布局视图,加载本地缓存,再加载网络资源