美文网首页
二十七、启动优化分析

二十七、启动优化分析

作者: KB_MORE | 来源:发表于2020-11-16 15:34 被阅读0次

    当用户按下home键的时候,iOS的App并不会马上被kill掉,还会继续存活若干时间。理想情况下,用户点击App的图标再次回来的时候,App几乎不需要做什么,就可以还原到退出前的状态,继续为用户服务。这种持续存活的情况下启动App,我们称为热启动,相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。我们这里只讨论App冷启动的情况。对于冷启动来说,启动时间是指从用户点击 APP 那一刻开始到用户看到第一个界面这中间的时间。

    冷启动
    • 内存中不包含APP的数据,所有数据都需要载入内存中,提供给应用使用。
      (ps: 内存中的数据是不会被删除的,但是存储空间可能被其他应用使用了,从而数据被覆盖。)
    热启动
    • 内存中仍然存在APP的数据,数据不需要重新载入内存。
      (ps: 当前应用所占的内存空间,未被其他应用覆盖。所以数据依旧可读取)
    冷启动与热启动的区别和场景

    【区别】内存中是否有加载的数据

    • 有:热启动,无需重新加载数据,速度快。
    • 无:冷启动,需要从磁盘读取数据加载到内存中,耗时,速度慢。

    【场景】

    • 首次启动: 一定是冷启动。(内存中无数据)
    • kill后启动:冷启动或热启动 (取决于内存中是否有数据)
    • 置于后台再回到前台: 冷启动或热启动 (取决于内存中是否有数据)
      (ps: 如果其他应用需要更多内存空间,系统可能自动覆盖你的内存空间提供给其他应用使用,此时你的数据就被覆盖了,回到前台时,应用自动重启)
    1. 启动性能检测和分析

    2. 启动性能检测和分析

    测试APP启动,分为两个阶段:

    系统处理,我们从dyld应用加载的流程来优化。(借助系统工具分析耗时)

    • main函数后开发者自己的业务代码

    通过检测业务流程优化main函数打个时间点第一个页面渲染完成打个时间点。测算耗时)

    3. APP启动时间优化原则

    对于启动时间优化其实就是遵循一个原则:尽早让用户看到首页内容
    根据这一原则将一些非必须的操作尽量往后移,通常是移到首页显示后执行,同时对于无法往后移的操作,尽可能不占用主线程,主线程尽量只做 UI 操作,将其他操作移到子线程。多利用CPU性能,在启动时发挥用多线程能力

    4. APP启动过程

    iOS应用的启动可分为pre-main阶段main()阶段,其中系统做的事情依次是:

    pre-main阶段

    • 解析Info.plist

      • 1.加载相关信息,例如如闪屏
      • 2.沙箱建立、权限检查
    • Mach-O加载

    • 1.如果是胖二进制文件,寻找合适当前CPU类别的部分

    • 2.加载所有依赖Mach-O文件(递归调用Mach-O加载的方法)

    • 3.定位内部、外部指针引用,例如字符串、函数等

    • 4.执行声明为__attribute__((constructor))的C函数

    • 5.加载类扩展(Category)中的方法

    • 6.C++静态对象加载、调用ObjC的 +load 函数

    main()阶段

    • 1.调用main()
    • 2.调用UIApplicationMain()
    • 3.调用applicationWillFinishLaunching

    5. 启动耗时的测量

    在进行优化之前,我们首先应该能测量各阶段的耗时。

    pre-main阶段测量

    在不越狱的情况下,以往很难精确的测量在main()函数之前的启动耗时,因而我们也往往容易忽略掉这部分数据。小型App确实不需要太过关注这部分。但如果是大型App(自定义的动态库超过50个、或编译结果二进制文件超过30MB),而我们的App的自定义动态库较多,且二进制文件接近60M,这部分耗时将会变得突出,需要我们优化pre-main阶段的时间。所幸,苹果已经在Xcode中加入这部分的支持。
    具体设置方法如下:

    在Xcode 中 Edit scheme -> Run ->Auguments 将环境变量DYLD_PRINT_STATISTICS设为1

    image

    还需要勾选下面这个选项:

    image

    设置好后把程序跑起来,控制台会有如下输出,pre-main阶段各过程的耗时从图上可以看出加载时间最长的阶段,咱们可以有针对性的检查并优化此过程。

    image

    5.1. 时间耗时解读

    main()函数之前总共使用了2000ms其中,加载动态库用了563.59.ms,指针重定位使用了910.80ms,ObjC类初始化使用了321.32ms,各种初始化使用了235.84ms。我们从上面的表中可以清晰的看出在哪个阶段耗时比较多,从而为下一步的优化提供指导。

    main()阶段测量
    对于main()阶段,主要是测量main()函数开始执行didFinishLaunchingWithOptions执行结束的耗时,就需要自己插入代码到工程中了。先在main()函数里记录当前时间,再在AppDelegate.m文件中的didFinishLaunchingWithOptions函数的最后获取一下当前时间,这两个的时间的差值即是main()阶段运行耗时。

    6. 优化思路及明确优化方向

    在通过以上方法测量出各阶段的占用时间,从数据上分析哪个阶段占用的时间多,从而指导我们明确优化的方向。

    是什么影响了我们的APP的启动时间?

    切忌挖空心思的研究优化main()函数调用之前的占用时间,反而忽略了-applicationDidFinishLaunching:函数之后那一堆堆臃肿的网络请求以及业务流程。所以我们先来看看-applicationDidFinishLaunching:函数之后,我们的APP都做了哪些事情:首先会初始化window,加载tabbar,加载首页controller以及数据,可能我们还有一个loading广告页,还有各种各样的业务需求,网络请求。所以这些都是需要去排查的地方,可以尝试通过添加打印时间戳的方式,来测量每个阶段的耗时情况。我们根据排查结果来明确造成启动缓慢的原因。

    7. pre-main阶段加载过程

    要对pre-main阶段的耗时做优化,需要再学习下dyld加载的过程,dyld的加载主要分为4步:

    7.1 加载dylibs

    这一阶段dyld会分析应用依赖的dylib(大部分是iOS系统的),找到其mach-o文件,打开和读取这些文件并验证其有效性,接着会找到代码签名注册到内核,最后对dylib的每一个segment调用mmap()

    7.2 Rebase/Bind

    这一阶段系统主要注册 Objc 类。所以,指针数量越少越好。

    7.4 Objc setup

    OC的runtime需要维护一张类名与类的方法列表的全局表。
    dyld做了如下操作:

    • 对所有声明过的OC类,将其注册到这个全局表中(class registration);
    • 将category的方法插入到类的方法列表中(category registration);
    • 检查每个selector的唯一性(selectoruniquing)

    在这一步倒没什么优化可做的,Rebase/Bind阶段优化好了,这一步的耗时也会减少。

    7.5 Initializers

    这一阶段,dyld 开始运行程序的初始化函数,调用每个 Objc 类分类+load 方法,调用 C/C++ 中的构造器函数。initializer阶段执行完后,dyld 开始调用 main() 函数。

    7.3 pre-main阶段耗时优化

    通过以上的pre-main阶段过程的分析我们得到如下结论:

    • 动态库加载越多,启动越慢;ObjC类越多,启动越慢;
    • C的constructor函数越多,启动越慢;C++静态对象越多,启动越慢;
    • ObjC的+load越多,启动越慢。
      实验证明,在ObjC类的数目一样多的情况下,需要加载的动态库越多,App启动就越慢。同样的,在动态库一样多的情况下,ObjC的类越多,App的启动也越慢。需要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个上升到100个的时候就会变得十分明显。同理,100个类和1000个类,可能也很难察觉得出,但1000个类和10000个类的分别就开始明显起来。
      因此我们建议在pre-main阶段的优化如下:
    • 1、尽量不要写__attribute__((constructor))的C函数,也尽量不要用到C++的静态对象;任何情况下,能用dispatch_once()来完成的,就尽量不要用到以上的方法。
    • 2、由于苹果对app二进制代码大小的限制,我们将app中很多基础控件和基础功能的静态库转成了很多个动态库,这导致了加载动态库时间耗费较长,为此我们将已有的多个动态库合并为一个动态库,减少加载dylib的时间。最好保持动态库的数量为6个及以下(系统推荐)
    • 3、排查清理项目中未使用到类库以及`Framework。
    • 4、清理项目中无用的类,删减没有被调用到或者已经废弃的方法。
    • 5、减少ObjC类(class)、方法(selector)、分类(category)的数量。
    • 6、删减一些无用的静态变量。
    • 7、检查 +load 方法,不要在+load方法里做耗时操作,尽量把事情推迟到 +initiailize 方法里执行, 也就是到使用时才加载。
    • 8、减少C++静态全局变量的个数。

    8main()阶段的耗时优化

    此阶段的优化才是我们app优化的核心与重点,大部分的启动时间消耗出现在此阶段:这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我们会创建应用的window,指定其rootViewController,调用window的makeKeyAndVisible方法让其可见。由于业务需要,我们会初始化各个三方库,推送、定位、im、埋点上报等基础服务的初始化,检查是否需要显示引导页、是否需要登录、是否有新版本等,由于历史原因,这里的代码容易变得比较庞大,启动耗时难以控制。所以,满足业务需要的前提下,didFinishLaunchingWithOptions在主线程里做的事情越少越好,可以把一些事情放在子线程去处理。
    优化方向及方案

    • 1、梳理各个三方库,找到可以延迟加载的库,做延迟加载处理,比如放到首页控制器的viewDidAppear方法里或者用到此功能的时候再去加载。
    • 2、梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
    • 3、复杂的计算(例如UI控件的位置信息及mode的解析)放到子线程中去处理。
    • 4、避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,部分可以延迟创建的视图应做延迟创建/懒加载处理。
      1. 首页控制器用纯代码方式来构建,xib及storyboard创建的界面第一次加载的时候相对来说要比纯代码加载速度稍慢。

    相关文章

      网友评论

          本文标题:二十七、启动优化分析

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