美文网首页iOS 开发每天分享优质文章
深入理解iOS App的启动过程

深入理解iOS App的启动过程

作者: CoderLF | 来源:发表于2019-01-15 16:13 被阅读25次

    前言

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


    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。

    比如网易新闻

    image.png

    在启动的时候只需要初始化首页的头条页面即可。像“要闻”,“我的”等页面,则延迟加载,即启动的时候只是一个UIViewController作为占位符给TabController,等到用户点击了再去进行真正的数据和视图的初始化工作。

    AppDelegate

    通常我们会在AppDelegate的代理方法里进行初始化工作,主要包括了两个方法:

    • didFinishLaunchingWithOptions
    • applicationDidBecomeActive

    优化这些初始化的核心思想就是:

    能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

    这些工作主要可以分为几类:

    • 三方SDK初始化,比如Crash统计; 像分享之类的,可以等到第一次调用再出初始化。
    • 初始化某些基础服务,比如WatchDog,远程参数。
    • 启动相关日志,日志往往涉及到DB操作,一定要放到后台去做
    • 业务方初始化,这个交由每个业务自己去控制初始化时间。

    对于didFinishLaunchingWithOptions的代码,建议按照以下的方式进行划分:

    @interface AppDelegate ()
    //业务方需要的生命周期回调
    @property (strong, nonatomic) NSArray<id<UIApplicationDelegate>> * eventQueues;
    //主框架负责的生命周期回调
    @property (strong, nonatomic) id<UIApplicationDelegate> basicDelegate;
    @end
    

    然后,你会得到一个非常干净的AppDelegate文件:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        for (id<UIApplicationDelegate> 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。

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


    image.png

    几点要注意:

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

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


    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的启动过程

        本文链接:https://www.haomeiwen.com/subject/majydqtx.html