美文网首页
第二七节课-启动优化

第二七节课-启动优化

作者: 飘摇的水草 | 来源:发表于2024-03-13 12:57 被阅读0次
    1. 启动时间检测

    这节课主要针对的是:

    • 冷启动:在内存中不包含相关的数据,必须从磁盘载入到内存中,这是由系统决定的,但是当APP的内存被覆盖的时候,APP也能决定

    对应的热启动则是:杀掉进程以后,数据仍然在,启动APP的时候不用重新加载数据

    • 测试APP启动以 main 函数为分界线,main 函数之前的监测一定要靠系统反馈,main 函数以后的则可以监测,这节课主要针对 main 函数之前的优化,因为 main 函数之后的操作是不统一的,根据业务来写的。

    如果想要了解 main 函数之前的耗时,是可以通过以下配置来监测,iOS APP的启动流程可以通过在xcode的Edit Scheme ->Run->Arguments -> Environment Variables中加入环境变量DYLD_PRINT_STATISTICS打印出来:

    配置.png

    运行APP,打印结果如下:

    Total pre-main time:  31.70 milliseconds (100.0%)
             dylib loading time:  27.29 milliseconds (86.0%)
            rebase/binding time: 411015771.5 seconds (61552300.3%)
                ObjC setup time:  78.12 milliseconds (246.4%)
               initializer time:  74.97 milliseconds (236.4%)
               slowest intializers :
                 libSystem.B.dylib :   6.10 milliseconds (19.2%)
       libBacktraceRecording.dylib :   7.03 milliseconds (22.1%)
                   libobjc.A.dylib :   1.69 milliseconds (5.3%)
        libMainThreadChecker.dylib :  57.82 milliseconds (182.3%)
    

    从结果可以看出四个阶段的名称以及它们对应的耗时。

    dylib loading 阶段是动态链接库dylib加载动态库的阶段,包括系统动态库和我们自己的动态库。
    rebase/binding这个阶段实际就是两个操作rebase和binding。rebase就是内部符号偏移修正,binding是外部符号绑定。
    ObjC setup这一阶段主要是OC类相关的事务,比如类的注册,category、protocol的读取等等。
    intializers 程序的初始化,包括所依赖的动态库的初始化。在这期间会调用 Objc 类的 + load 函数,调用 C++ 中带有constructor 标记的函数等。

    上面打印出的这四个阶段还是比较粗略的,实际上dyld在启动过程中是比较复杂的。如果将上面的DYLD_PRINT_STATISTICS换成DYLD_PRINT_STATISTICS_DETAILS,打印得会更加详细:

    total time: 1.3 seconds (100.0%)
      total images loaded:  398 (392 from dyld shared cache)
      total segments mapped: 21, into 415 pages
      total images loading time: 944.18 milliseconds (68.5%)
      total load time in ObjC:  80.88 milliseconds (5.8%)
      total debugger pause time: 801.87 milliseconds (58.2%)
      total dtrace DOF registration time:   0.00 milliseconds (0.0%)
      total rebase fixups:  16,279
      total rebase fixups time:   7.47 milliseconds (0.5%)
      total binding fixups: 567,346
    
      total binding fixups time: 275.57 milliseconds (20.0%)
      total weak binding fixups time:   0.03 milliseconds (0.0%)
      total redo shared cached bindings time: 459.30 milliseconds (33.3%)
      total bindings lazily fixed up: 0 of 0
      total time in initializers and ObjC +load:  68.67 milliseconds (4.9%)
                             libSystem.B.dylib :   6.04 milliseconds (0.4%)
                   libBacktraceRecording.dylib :   7.01 milliseconds (0.5%)
                    libMainThreadChecker.dylib :  52.05 milliseconds (3.7%)
    total symbol trie searches:    1378030
    total symbol table binary searches:    0
    total images defining weak symbols:  50
    total images using weak symbols:  110
    

    main 函数之后的优化有几个点:

    • 懒加载
    • 删除不再使用的代码,即便那些代码不在使用,但在它们仍然参与到了编译中,因此也会消耗一部分时间
    • 首屏页面最好不要使用 XibStoryBoard,因为它们本身就需要解析
    2. 启动优化:二进制重排

    二进制重排原理:虚拟内存和物理内存,早期的计算机只有物理内存,内存条的地址叫做物理地址,早期的数据访问都是通过物理地址来访问的,这样子有个问题就是拿到内存地址就可以对数据进行修改,因此存在数据问题和内存不够用的问题。操作系统发现软件虽然很大,但是实际上用户只使用了部分活跃功能,所以把应用的全部数据载入内存有点浪费,因此虚拟内存就出现了,虚拟内存就是让应用觉得自己被全部载入了内存,实际上并没有,每个应用都有一个虚拟的内存,cpu首先访问虚拟地址,虚拟地址和物理地址会有一个对应

    物理内存 & 虚拟内存

    物理内存:指的是通过物理内存条获得的内存空间。
    虚拟内存:跟物理内存相反,虚拟内存指的一种计算机系统内存管理技术,它使得应用程序认为它拥有连续可用的内存,实际上它通常被分隔成多个物理内存碎片。
    只谈概念太空洞,下面我们用图来解释什么是物理内存、什么是虚拟内存。
    物理内存的概述图如下:

    物理和虚拟内存.png

    分析:
    在没有虚拟内存的概念之前,每个应用一启动,操作系统就会把整个应用放进物理内存里面

    物理内存存在的问题:

    • 内存紧张 - 由于每次都是直接把整个应用放进物理内存里面,很可能出现内存不够用的情况。
    • 进程安全 - 每个进程之间的物理地址是连续的,可以拿到别的进程的地址,容易出现进程不安全的问题。
      虚拟内存的概述图如下:
    虚拟内存.png

    分析:
    虚拟内存的技术出现之后,每个进程并不是直接全部扔进物理内存,而是给每个应用分配一个虚拟的内存,虚拟内存通过虚拟页表来把相应数据放进物理内存里面。

    虚拟内存的技术出现之后,也有了内存分页的概念,虚拟页表把一个进程分成若干页,比如:Page1、Page2、Page3......,当启动进程1的时候,只需要把Page1装载进物理内存,以此类推,如上图。
    PS:大家注意上图的颜色区分能够很好的理解虚拟内存相关概念。

    虚拟内存解决了物理内存存在的两个问题,由于每个进程的数据被分成了很多页装载进内存,不会再出现内存紧张的问题,而且一个应用在物理内存里面的地址是不连续的,所以无法访问到别的进程的地址,也保证了线程安全。
    注意:虽然一个应用在物理内存中的内存是不连续的,但是访问数据的时候是连续的,原因就在于我们访问数据访问的是对应的虚拟映射表,这个虚拟映射表记录的某个方法、函数对应在物理内存的真实地址。

    缺页中断(PageFault)

    在上面我们说过在有了虚拟内存技术之后,内存以分页的形式装载进物理内存里面,比如我们现在启动一个应用只需要先把启动相关的内存Page1到Page10装载进物理内存,当用到A功能的时候发现数据没有在物理内存里面,此时操作系统会阻塞APP进程触发一次PageFault,操作系统会从磁盘中读取相应的数据到物理内存上,再将其映射到虚拟内存页表上面。PageFault耗时非常短,平均在0.5ms左右,所以用户一般感知不到,但是有一种情况可能会造成大量的PageFault,那就是启动时刻,所以我们就可以思考如何减少启动时的PageFault次数,从而优化启动时长。

    二进制重排

    在分析二进制重排之前,我们先了解一下 Link Map File 是个什么东西。

    链接映射文件

    链接映射文件:Link Map File,里面记录的是每个类所生成的可执行文件的路径、CPU架构、符号等信息,可以简单的理解为这个文件告诉了我们一个应用的可执行文件的排列顺序。

    BuildSetting - Write Link Map File设置为YES。

    如下图所示:

    linkmap设置.png

    编译项目之后根据上图的地址找到我们需要的 Link Map 文件,如图所示:

    linkmap文件.png

    linkmap 文件内方法的排列顺序遵循两种规则:

    1. 文件的编译顺序,即:先按照项目 - Build Phases - Compile Sources中的顺序排列,如下图所示:
    编译顺序.png
    1. 同一个文件内部则遵循从上至下的方法书写顺序排列

    这样的排列顺序就导致:启动时刻的代码顺序分别分布在了不同的文件里面,即启动代码并没有在一起,导致启动时刻有大量的 pageFault

    优化思路:将所有启动时刻需要调用的方法排列在一起,这就是二进制重排。

    缺页中断个数

    在进行优化之前,我们首先需要检查一下优化之前的缺页中断个数,打开 Instruments,选择 System Trace,运行之后。分析数据如图,选择“Main Thread”,底部的File Backed Page In即为缺页中断个数。

    所下图所示

    systemTrace.png

    当我们测试过一次后再打开 System Trace,发现缺页中断个数少了很多,这个主要是由于缓存造成的,要想完完整整地去测试冷启动的缺页中断次数的话,可以在杀死app之后再打开几个其他的APP,然后再过个一两分钟之后,再启动这个APP的话,就应该是冷启动了。

    相关文章

      网友评论

          本文标题:第二七节课-启动优化

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