前言
参考资料:
深入理解iOS App的启动过程
iOS 应用程序启动过程及原理总结
iOS:App启动过程详解(不同方式)
iOS APP启动函数调用顺序~详解
[]
启动时间
冷启动 VS 热启动
如果你刚刚启动过App,这时候App的启动所需要的数据仍然在缓存中,再次启动的时候称为热启动。如果设备刚刚重启,然后启动App,这时候称为冷启动。
启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。
在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS 和 DYLD_PRINT_STATISTICS_DETAILS
。
Total pre-main time: 43.00 milliseconds (100.0%)
dylib loading time: 19.01 milliseconds (44.2%)
rebase/binding time: 1.77 milliseconds (4.1%)
ObjC setup time: 3.98 milliseconds (9.2%)
initializer time: 18.17 milliseconds (42.2%)
slowest intializers :
libSystem.B.dylib : 2.56 milliseconds (5.9%)
libBacktraceRecording.dylib : 3.00 milliseconds (6.9%)
libMainThreadChecker.dylib : 8.26 milliseconds (19.2%)
ModelIO : 1.37 milliseconds (3.1%)
对于这个libMainThreadChecker.dylib估计很多同学会有点陌生,这是XCode 9新增的动态库,用来做主线成检查的。
优化启动时间
启动时间这个名词,不同的人有不同的定义。在我看来,
启动时间是用户点击App图标,到第一个界面展示的时间。
以main函数作为分水岭,启动时间其实包括了两部分:main函数之前和main函数到第一个界面的viewDidAppear:。所以,优化也是从两个方面进行的,个人建议优先优化后者,因为绝大多数App的瓶颈在自己的代码里。
Main函数之后
我们首先来分析下,从main函数开始执行,到你的第一个界面显示,这期间一般会做哪些事情。
- 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions
- 初始化Window,初始化基础的ViewController结构(一般是UINavigationController+UITabViewController)
- 获取数据(Local DB/Network),展示给用户。
UIViewController
延迟初始化那些不必要的UIViewController。
比如网易新闻:
在启动的时候只需要初始化首页的头条页面即可。像“要闻”,“我的”等页面,则延迟加载,即启动的时候只是一个UIViewController作为占位符给TabController,等到用户点击了再去进行真正的数据和视图的初始化工作。
AppDelegate
通常我们会在AppDelegate的代理方法里进行初始化工作,主要包括了两个方法:
- didFinishLaunchingWithOptions
- applicationDidBecomeActive
优化这些初始化的核心思想就是:
能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。
这些工作主要可以分为几类:
- 三方SDK初始化,比如Crash统计; 像分享之类的,可以等到第一次调用再出初始化。
- 初始化某些基础服务,比如WatchDog,远程参数。
- 启动相关日志,日志往往涉及到DB操作,一定要放到后台去做
- 业务方初始化,这个交由每个业务自己去控制初始化时间。
对于didFinishLaunchingWithOptions的代码,建议按照以下的方式进行划分:
@interface AppDelegate ()
//业务方需要的生命周期回调
@property (strong, nonatomic) NSArray> * eventQueues;
//主框架负责的生命周期回调
@property (strong, nonatomic) id basicDelegate;
@end
然后,你会得到一个非常干净的AppDelegate文件:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
for (id delegate in self.eventQueues) {
[delegate application:application didFinishLaunchingWithOptions:launchOptions];
}
return [self.basicDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
由于对这些初始化进行了分组,在开发期就可以很容易的控制每一个业务的初始化时间:
CFTimeInterval startTime = CACurrentMediaTime();
//执行方法
CFTimeInterval endTime = CACurrentMediaTime();
用Time Profiler找到元凶
Time Profiler在分析时间占用上非常强大。实用的时候注意三点
- 在打包模式下分析(一般是Release),这样和线上环境一样。
- 记得开启dsym,不然无法查看到具体的函数调用堆栈
- 分析性能差的设备,对于支持iOS 8的,一般分析iphone 4s或者iphone 5。
一个典型的分析界面如下:
几点要注意:
- 分析启动时间,一般只关心主线程
- 选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码
- 右侧可以看到详细的调用堆栈信息
在某一行上双击,我们可以进入到代码预览界面,去看看实际每一行占用了多少时间:
小结
不同的App在启动的时候做的事情往往不同,但是优化起来的核心思想无非就两个:
- 能延迟执行的就延迟执行。比如SDK的初始化,界面的创建。
- 不能延迟执行的,尽量放到后台执行。比如数据读取,原始JSON数据转对象,日志发送。
Main函数之前
Main函数之前是iOS系统的工作,所以这部分的优化往往更具有通用性。
dylibs
启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。
- 合并动态库,比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度。
Rebase & Bind & Objective C Runtime
Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:
- 减少__DATA段中的指针数量。
- 合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个
- 删除无用的方法和类。
- 多用Swift Structs,因为Swfit Structs是静态分发的。感兴趣的同学可以看看我之前这篇文章:《Swift进阶之内存模型和方法调度》
Initializers
通常,我们会在+load方法中进行method-swizzling,这也是Nshipster推荐的方式。
- 用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。
- 减少atribute((constructor))的使用,而是在第一次访问的时候才* * 用dispatch_once等方式初始化。
- 不要创建线程
- 使用Swfit重写代码。