IOS-启动优化(上)

1,973 阅读7分钟
  • 应用的启动时间直接影响着用户对应用的第一印象和使用体验,尤其是一些接入很多第三方框架和启动阶段业务比较复杂的应用。所以我们需要对启动时间进行优化。

启动优化涉及的知识点

  • 冷启动和热启动
  • ios启动性能检测
  • 物理内存和虚拟内存
  • 二进制重排原理
  • iosPageFault检测分析

冷启动和热启动

  • 冷启动(cold launch):app长时间没启动或者重新开机之后启动的app就是冷启动,也就是说内存中没有app的数据,dyld中也没有缓存。
  • 热启动(warm launch):当你的app启动过一次之后,这种情况还是需要唤醒,只是在内存中已经有一些系统级服务了,所以启动会快很多。
  • 继续运行(resume):app还没有被挂起,打开之后继续运行。

ios启动性能检测

  • 通过Edit scheme->Run->Arguments添加环境变量DYLD_PRINT_STATISTICS

  • 运行程序,打印出如下数据,这些属于main函数之前的时间,也就是pre-main

Total pre-main time: 196.68 milliseconds (100.0%)
         dylib loading time: 156.23 milliseconds (79.4%)
        rebase/binding time: 126687488.9 seconds (359436379.6%)
            ObjC setup time:  13.83 milliseconds (7.0%)
           initializer time:  39.41 milliseconds (20.0%)
           slowest intializers :
             libSystem.B.dylib :   5.49 milliseconds (2.7%)
   libBacktraceRecording.dylib :   4.66 milliseconds (2.3%)
    libMainThreadChecker.dylib :  24.93 milliseconds (12.6%)

Total pre-main time

  • 整个pre-main花费的时间

dylib loading time

  • 动态库的加载时间,由于系统的动态库应该做过优化了,我们主要是对自己调用的第三方动态库优化,苹果建议自定义的动态库不要超过6个,我们尽量减少使用自定义的动态库或者合并动态库

rebase/binding time

  • rebase:偏移修正 我们编写的代码的方法,函数等都有对应的地址在二进制文件中,这个地址是一个偏移地址,加载进内存的时候由于ASLR技术会加上一个随机值才能访问它们的真实地址。

ASLR其实就是Address Space Layout Randomization,地址空间布局随机化。它是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。在iOS 4.3开始引入ASLR技术

  • binding符号绑定 我们平常调用的系统方法比如NSLog,会在mach-o文件中创建一个随机符号,在运行到内存的时候会将NSLog真正的地址和符号关联,这个关联就叫绑定。

ObjC setup time

  • 这一步做的主要操作就是类的加载,分类的合并,保证每个seletor的唯一
  • 优化方案:合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类如下,清除一些没用的静态变量,清除一些没有用到的方法

initializer time

  • 这里主要做的是load和构造函数(这里的构造函数是指c++)的实现
  • 优化方案:推迟到启动之后,尽量少使用load方法和c++的构造函数,将不必须在+load方法中做的事情延迟到+initialize中

main函数之后的优化

  • 懒加载
  • 无关的业务代码,已经不再使用的文件
  • 启动刚进来的界面尽量不要使用xib、storeboard

物理内存和虚拟内存

物理内存

  • 在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。单运行的时候没有什么问题!随着时间的法杖计算机会有多程序、分时系统和多任务,当我们能够同时运行多个程序时,CPU的利用率将会比较高。那么有一个非常严重的问题:如何将计算机的有限的物理内存分配给多个程序使用 早期的计算机内存分配
  • 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。
  • 进程地址空间不隔离,由于程序是直接访问物理内存的,所以每一个进程都可以修改其他进程的内存数据,设置修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会有内存不安全的问题
  • 内存使用效率低 内存空间不足,就需要将其他程序展示拷贝到硬盘当中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率会非常低,但其实应用在使用过程中并不是全部活跃的。
  • 程序运行的地址不确定;因为内存地址是随机分配的,所以程序运行的地址也是不正确的。

虚拟内存

  • 为了解决上述问题,于是有了虚拟内存的概念
  • cpu通过先访问虚拟内存,然后通过地址翻译(MMU内存管理单元配合操作系统找物理地址)将虚拟地址转化为逻辑物理地址
  • 内存分页管理,由于app在使用过程中都是只有部分活跃的,并不需要将它全部载入内存,所以就设计出了分页,分页就是将内存分割成相同大小的页,一般UNIX/macOs上面每页都是4kb,在ios上每页16kb。
  • cpu通过页表访问内存,当访问的页有对应的逻辑物理地址的时候就直接访问物理内存,而当页没有对应的地址就会触发一个中断也就是我们常说的缺页中断也叫缺页异常(PageFault)。
  • 当我们的物理内存都被使用时,就会覆盖掉原来不活跃的数据,这也是我们在使用手机的时候挂起的程序会被杀掉的原因
  • 由于虚拟内存都是从0开始的,每次运行时候的虚拟地址都是相同的,所以有了ASLR技术

二进制重排

  • 通常我们遇到PageFault的时候,计算机没有分配物理地址,cpu会很快处理,处理时间很快所以我们察觉不出,而当有大量的PageFault出现的时候就会有比较长的处理时间。
  • 通常会出现大量PageFault的时候是在程序刚刚启动的时候,所以启动优化就有了二进制重排相关的优化
  • 我们在启动时刻会将要执行的页加入到内存中,有时候我们16kb的数据只有1kb的数据或者更少的数据是要在pre-main之前加载的,当我们启动时刻要加载的方法分布在不同的页内,就有了优化空间,我们将在启动时刻需要使用的符号放到一起,就可以减少启动时刻的PageFault。

iosPageFault检测分析

默认的符号排列顺序

  • ios中符号的默认加载顺序是按照文件编译顺序,再根据文件里面内部书写顺序排列的。
  • 通过一个项目来验证下符号的排列,我们在vc里面实现一些方法,通过Build Settings中搜索link Map,设置Write Link Map FileYES
  • 编译之后,找到编译完的.app文件,右键Show In Finder找到当前文件的上两级文件中的Intermediates.noindex,往下按路径找到一个.txt的文件
  • 打开.txt文件,就可以看到符号的排列了
  • 编译顺序
  • ViewController中的书写顺序

二进制重排之后的符号顺序

  • 我们打开创建的测试Demo项目,把排序改成load->touchesBegan:withEvent:->viewDidLoad->_block1_block_invoke->test1
    • 在demo的根目录新建一个test.order文件
    • 在文件中按照上述顺序添加符号,并添加了一个不存在的方法
    +[ViewController load]
    -[ViewController touchesBegan:withEvent:]
    -[ViewController viewDidLoad]
    _block1_block_invoke
    -[ViewController hello]
    -[ViewController test1]
    
    • Build Settings中搜索order file,加入./test.order
    • 再次编译后,我们按照上面的路径打开.txt文件

  • 我们可以看到符号的排列就会按照我们书写顺序排列的,没有的会自动跳过,剩下的按照默认顺序排列
  • 虽然我们已经知道了二进制重排可以减少启动时间,但是我们怎么获取启动之前调用的符号呢,就需要用到我们下一篇要用的clang插桩技术

iosPageFault检测分析

  • 利用Xcode自带的System Trace分析我们一个项目在启动时候的PageFault
  • 右键Xcode打开Instruments,找到System Trace
  • 调试结果如下