深入理解iOS App的启动过程

前言

参考资料:
深入理解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


深入理解iOS App的启动过程_第1张图片
image.png
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。

比如网易新闻:

深入理解iOS App的启动过程_第2张图片
image.png

在启动的时候只需要初始化首页的头条页面即可。像“要闻”,“我的”等页面,则延迟加载,即启动的时候只是一个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。

一个典型的分析界面如下:


深入理解iOS App的启动过程_第3张图片
image.png

几点要注意:

  • 分析启动时间,一般只关心主线程
  • 选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码
  • 右侧可以看到详细的调用堆栈信息

在某一行上双击,我们可以进入到代码预览界面,去看看实际每一行占用了多少时间:


深入理解iOS App的启动过程_第4张图片
image.png

小结

不同的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重写代码。

你可能感兴趣的:(深入理解iOS App的启动过程)