启动过程分析
-
解析Info.plist
- 加载相关信息,例如闪屏
- 沙箱建立、权限检查
-
Mach-O加载
Mach-O 文件:我们写的程序想要跑起来,肯定它的可执行文件格式要被操作系统所理解。比如ELF是Linux下的可执行文件格式,那么对于OS X / iOS来说,Mach-O 是其可执行文件格式。
Mach-O格式主要包括以下几种文件类型:
- Executable:应用的主要二进制
- Dylib:动态链接库
- Bundle:不能被链接,只能在运行时使用dlopen加载
- Image:包含Executable、Dylib和Bundle
- Framework:包含Dylib、资源文件和头文件的文件夹
- 如果是胖二进制文件,寻找合适当前CPU类别的部分
- 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
- 定位内部、外部指针引用,例如字符串、函数等
- 执行声明为
__attribute__((constructor))
的C函数 - 加载类扩展(Category)中的方法
- C++静态对象加载、调用ObjC的
+load
函数
-
程序执行
- 调用
main()
- 调用
UIApplicationMain()
- 调用
applicationWillFinishLaunching
- 调用
测量App的启动时间
iOS App的启动有两种方式:冷启动和热启动,下面对这两种启动方式进行简单介绍。
冷启动:App第一次启动或者是被上滑Kill掉之后,再次从点击App应用图标到App完全启动显示主页面为止的过程成为冷启动。
热启动:当用户从当前App按下HOME键退出后,系统并不会立即kill掉App的进程,还会继续后台执行一段时间(至于什么时候会被kill,完全取决于系统内存等资源的使用情况,理想情况下可以保持后台运行15min)。然后当用户再次点击App图标时,会很快加载到上次停留的页面,几乎不需要进行资源载入,这种情况下的启动成为热启动。
下面我们对于App启动时间的测量只讨论在冷启动情形下。根据苹果官方介绍,iOS应用的启动分为两个阶段:pre-main
和main
,所以App启动的总时间为:Total Time = pre-main Time + main Time。
Pre-main 阶段
Pre-mian 阶段即main()
函数被调用之前的加载时间,包括dylib动态库的加载、Mach-O文件加载、Rebase/Bining、Objective-C Runtime 加载等。该过程如下图所示:
main 阶段
main 阶段指的是从调用main()
函数开始,调用UIApplicationMain()
创建UIApplication,AppDelegate
,直到回调App代理方法applicationDidBecomeActive:
为止,其中包括application:didFinishLaunchingWithOptions
方法中创建keyWindow
、三方sdk和其他初始化项所消耗的时间,也包含RootViewController 的 ChildViewController的view完全显示出来所花费的时间。该过程如下图所示:
pre-main 阶段时间测量
那么对于pre-main 和 main 启动阶段的耗时如何测量呢?苹果已为我们提供了一个简单易用的测试方法:在Xcode中选中项目的Scheme
→ Edit Scheme...
→,然后选择Run
→Arguments
→Environment Variables
中添加Name = DYLD_PRINT_STATISTICSvalue
,Value = 1 的环境变量。如下图所示:
然后重新运行项目,注意控制台下面的打印输出,格式为:
Total pre-main time: 655.68 milliseconds (100.0%)
dylib loading time: 205.67 milliseconds (31.3%)
rebase/binding time: 320.95 milliseconds (48.9%)
ObjC setup time: 63.07 milliseconds (9.6%)
initializer time: 65.85 milliseconds (10.0%)
slowest intializers :
libSystem.B.dylib : 3.43 milliseconds (0.5%)
libMainThreadChecker.dylib : 15.59 milliseconds (2.3%)
century : 76.56 milliseconds (11.6%)
// 内容解读
Total pre-main time: 表示pre-main阶段总的时间消耗为655.68毫秒
dylib loading time: 动态库加载时间
rebase/binding time: 重定位指针指向/指向镜像外部内容 所需时间
ObjC setup time: ObjC初始化时间
initializer time: 其他初始化时间的总和
slowest intializers : 最慢的三个初始化分别是libSystem.B.dylib、libMainThreadChecker.dylib、century(项目名称)
main 阶段时间测量
由于main阶段涉及到大量的自定义代码,app主体页面的初始化、前期配置、三方库初始化等大量逻辑代理,iOS系统没有直接的测量方式,所以只能通过打点的方式计算启动开始到启动结束的时间差即可。例如下面示例代码:
// main.m 文件
#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 文件
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);
});
}
影响启动性能的因素
接下来我们讨论在App启动的pre-main阶段和mian阶段,影响启动性能的多种因素。
Pre-main 阶段
- 动态库加载越多,启动越慢。
- ObjC类越多,启动越慢
- C的constructor函数越多,启动越慢
- C++静态对象越多,启动越慢
- ObjC的
+load
方法使用越多,启动越慢
我们尽量不要写__attribute__((constructor))
的C函数,也尽量不要用到C++的静态对象;至于ObjC的+load
方法,似乎大家已经习惯不用它了。任何情况下,能用dispatch_once()
来完成的,就尽量不要用到以上的方法。
main 阶段
- 执行main()函数的耗时
- 执行application:didFinishLaunchingWithOptions的耗时
- rootViewController及其childViewController的加载、view及其subviews的加载
一般的App的主体UI架构是,UITabbarViewController
作为 keyWindow
的根控制器,然后包含多个ChildViewController
作为每个tab的根控制器。然而在didFinishLaunchingWithOptions
代理方法中各控制器的初始化顺序是怎样的呢?
答案是:
-[MQQTabBarController viewDidLoad]
-[MQQTab1ViewController viewDidLoad]
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[MQQTab2ViewController viewDidLoad]
(点击了第二个tab之后加载)-[MQQTab3ViewController viewDidLoad]
(点击了第三个tab之后加载)
从上面的加载过程可以看出,在main()方法之后的启动中,只要是对页面的渲染,要减少这个阶段的耗时需要在viewDidLoad
方法中做优化,减少不必要的逻辑,视图尽量使用懒加载,使用异步方式加载网络数据。
启动时间优化
苹果建议在400ms内完成pre-main 阶段的启动,App整体的启动时间不能超过20s,否则系统会被kill掉进程。
由于每个App的体量和类型不一样,对应启动过程中配置的逻辑处理复杂程度也是皆不相同,所有没有一个标准的时间来衡量启动时间是否达到最优。但是可以从用户体验上下功夫预加载占位视图,数据缓存等都可以营造出加载速度提升的感觉。
下面从实践的角度分析可优化的部分:
pre-main 阶段优化
-
移除不需要的动态库framework
-
移除不需要的类、未使用的变量、方法等
对应项目中大量的文件,可以使用工具快速扫描整个项目找出未使用的类、变量、或方法。这里推荐一个开源工具 Fui 能准确的完成此任务,不足之处在于它处理不了动态库或静态库中提供的类,和C++ 的类模板。
Fui使用方法:
在终端中cd到项目根目录,然后执行
fui find
,就可以得到一个列表,可根据这个列表在Xcode中手动检查并删除无用代码。 -
合并功能类似的类和扩展(Category)
-
+load
方法中做的事情可以使用dispatch_once()
来代替,因为main()
方法被调用之前就会加载+load
方法,会影响pre-main
阶段启动事件。 -
压缩资源图片(对要求不高的图片可做压缩处理)
main 阶段优化
- 优化
application: didFinishLaunchingWithOptions:
内部逻辑- 各种业务请求配置更新,多个配置更新可合并为单个请求,部分业务的配置延后更新。
- 对于新版本引导、广告闪屏逻辑,需要重构逻辑清晰
- 数据迁移,对太旧的无用的逻辑可以删除,不需要立即处理的数据,改为后台加载
- 优化rootViewController加载:在启动过程中只加载tabbarVc,主Vc即可,而且主Vc中的
ViewDidLoad
方法中也只加载需要立即显示出来的view,其他视图均使用懒加载,数据进行异步加载。