美文网首页iOSiOS开发iOS源码解析
iOS - 优化App冷启动速度

iOS - 优化App冷启动速度

作者: 冰风v落叶 | 来源:发表于2019-04-04 16:12 被阅读427次

    1. App的启动分为三个主要阶段:

    • main()函数执行前

    • main()函数执行后(从main函数执行,到设置self.window.rootViewController)

    • 首屏渲染完成后(从设置self.window.rootViewController到didFinishLaunchWithOptions方法作用域结束)

    main函数执行前,系统会做的事情:
    • 加载可执行文件(App的.o文件集合)

    • 加载动态链接库,进行rebase指针调整和bind符号绑定

    • Objc运行时的初始处理,包括Objc相关类注册、category注册、selector唯一性检查等

    • 初始化,包括了执行+load()方法、attribute((constructor))修饰的函数的调用、创建C++静态全局变量。

    main()函数执行后:

    main()函数执行后的阶段,指的是从main()函数执行开始,到appDelegate的didFinishLaunchingWithOpentions方法里首屏渲染相关方法执行完成。

    这里应该是从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是App启动必要的初始化功能,哪些是只需要在对应功能开始使用时才需要初始化的,将这些放到各自合适的阶段执行。

    首屏渲染完成后:

    首屏渲染后的这个阶段,指的是didFinishLaunchWithOptions方法作用域内执行首屏渲染之后的所有方法执行完成,即从 设置了self.window.rootViewController开始 到 didFinishLaunchWithOptions方法作用域 结束。

    首屏渲染完成后用户就可以看到App的首页信息了,把这个阶段内卡住主线程的方法解决掉就可以了。

    注解:
    • App启动后,首先加载可执行文件,然后加载dyld,然后加载所有依赖库,然后调用所有的+load(),然后调用main(),然后调用UIApplicationMain(),然后调用AppDelegate的代理didFinishLaunchWithOptions.

    • 可执行文件是指Mach-O格式的文件,也就是App中所有.o文件的集合体,从这里可以获取dyld的路径,然后加载dyld。

    • dyld是指苹果的动态链接器,加载dyld后,就会去初始化运行环境,开启缓存策略,加载依赖库,并且会调用每一个依赖库的初始化方法,包括RunTime也是在这里被初始化的,当所有的依赖库都被初始化完成后,RunTime会对项目中所有的类进行类初始化,调用所有的+load()方法,最后dyld会返回main函数地址,然后main函数会被调用。

    • 知晓上述的流程后,我们就明白为什么优化启动速度,要去减少动态库加载,要少用+load(),理论明白了之后,我们就要看看具体怎么做了。

    • 动态库是指可以共享的代码文件、资源文件、头文件等的打包集合体。在Xcode->Targets->General->Link Binary With Libraries可以检查自己的库,

    • 减少+load()的使用,将里面的内容放到渲染结束后去做,或者用+initialize()代替。+load()方法在main()调用前就会调用,而+initialize()方法是在类第一次收到消息后,才会调用,两者的区别可以参考这里

      Main函数调用前
      Main函数调用后

    2.具体优化方法

    (1)减少+load()的使用

    使用+initialize()的方法代替+load(),注意把逻辑移动到+initialize()时,要注意避免+initialize()的重复调用问题,可以使用dispatch_once()让逻辑只执行一次。

    (2)对多个动态库进行合并

    苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持6个非系统动态库合并为一个。

    (3)优化类、方法、全局变量

    减少加载启动后不会去使用的类或方法;控制C++全局变量的数量

    (4)功能级别的启动优化

    main()开始执行后到首屏渲染完成前,只处理首屏相关的业务,其他的都放到首屏渲染完成后去做。

    (5)方法级别的启动优化

    首先检查首屏渲染完成前主线程上的耗时操作,将没必要的操作滞后或异步。通常耗时操作有:加载、编辑、存储图片和文件等资源。

    3. 查看耗时

    (1)查看Main()调用前花费的总时间

    在Product->Scheme->Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS设置为YES,就可以在控制台中查看main函数执行前总共花费的多长时间。

    设置环境变量.png
    控制台会输出pre-main的总时间.png
    (2)查看加载了多少动态库

    在Product->Scheme->Edit Scheme->Run->Diagnostics->Logging->勾选Dynamic Library Loads,就可以在控制台中查看本项目中加载的所有动态库(包括系统的和自己的)。


    image.png
    (3)查看Main函数启动后的耗时

    main函数调用后的耗时,可以使用一些工具来监控,有一种非常笨但是很实用的方法,就是通过打点,在didFinishLaunchingWithOptions开始前打一个点,在App显示完成第一个界面再打一个点,计算两个点之间的耗时,就可以知道main函数调用后到界面显示出来的耗时了,但是这样只能笼统的知道总的耗时,并不能准确的知道时间花在了哪里。

    如果想用这个打点法的话,推荐一个打点工具BLStopwatch

    如果想准确知道时间都花在了哪里,推荐使用下面两种方法。

    4. 监控App启动耗时,精准找出时间都花在了哪里,方便逐一优化

    准确监控方法有两种:
    1. 定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。Xcode自带的Time Profiler就是用的这种方法。

    2. 对objc_msgSend方法进行hook来掌握所有方法的执行耗时。

    根据这两种方法,分别实现两个工具,来监控耗时

    由于能力有限,我只根据第一种方法做出来一个计算某个线程的耗时工具,放在了这里BSMonitorTimeTool,大致思路如下:

    (1). 通过定时器,每隔0.01s,获取一次主线程的函数堆栈,将函数名称、函数地址、函数耗时模型化为TimeModel,保存在callStackDict中,其中key为函数地址,value为TimeModel

    (2). 定时执行的回调中,每次都判断函数地址是否存在,如果已经存在此函数地址,就讲对应的TimeModel中的耗时增加0.01s;如果不存在此函数地址,就初始化一个TimeModel,并将时间设置为0.01s。

    (3). 当主界面显示完成之后,输出此callStackDict,即可查看主线程中每个方法的耗时

    5. 欢迎大家指正错误,希望能够共同进步

    本文章是参考了很多大佬的文章,欢迎各位前去膜拜

    相关文章

      网友评论

        本文标题:iOS - 优化App冷启动速度

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