iOS 启动优化(上)

1,451 阅读7分钟

当我们的APP随着业务的增加、复杂,代码量也随之暴增,慢慢的打开我们的App时感觉非常卡,启动比较缓慢,非常影响用户的体验,那么如何才能使我们的App启动比较流畅,给用户很好的体验,这篇文章将给大家带来App启动优化相关的知识。

1 App启动流程分析

App的启动我们一般分为两个部分:

  • main函数之前即pre-main
  • main函数之后

1.1 pre-main阶段流程

DYLD 环境变量DYLD_PRINT_STATISTICS监测一下pre-main的时间消耗,我们在xcode中设置一个参数,如图:

Xnip2022-08-01_16-13-55.png

我们启动App,看下输出结果,如下图 Xnip2022-08-01_16-18-09.png

共耗时 300ms 左右:

  • dylib loading 加载动态库的时间

  • rebase/binding 重定向/绑定的时间

  • ObjC setup OC类的注册时间

  • initializer 注册方法的时间(load,构造函数的耗时)

这便是pre-main的基本流程

1.2 dylib动态的加载

dylib的加载的耗时是必然的,系统的动态库是已经载入共享缓存空间,系统动态库已经做了高速的优化, 但是我们自定义的动态库不一样,所以苹果不建议自定义动态库

1.3 ObjC setup

因为我们的OC是动态语言,OC类的注册

  • 读取Mach-odata字段,找到OC类的相关信息
  • 注册OC类,OCruntime需要维护映射表即SEL/IMP的映射以及类名与类的全局表,当加载Mach-o的时候,这些所有的类都要注册到全局表中,除这些之外,还有类别协议信息要插入到方法列表中,这是必然的损耗,所以这里的优化删除无用的OC的类文件(只要这个类存在即使没有用到,也会造成时间损耗)

1.4 initializer

load方法以及构造函数中,尽量不要做延迟加载的事情,把消耗的任务放在子线程去,以减少主线程的开销,数据可以缓存。

以上几点的优化都比较简单,下理我们来介绍rebase/binding 重定向/绑定,再介绍之前,我们先来讲来虚拟内存相关的知识

2 物理/虚拟内存介绍

物理内存

  • 在早期的操作系统,CPU直接从内存条读取数据,这就导致了内存不够用问题,如下图:

    截屏2021-09-02 14.30.02.png

    • 应用需要加载时就会直接加载到内存条中,然后CPU去内存条读取。当加载的应用多了,再加载新的应用时内存就不够用了,这时候就需要干掉一些应用然后再去打开这个新的应用。

    • 加载到内存条中的应用都是根据内存地址去读,那么如果通过一些外挂加载到内存,然后可以在遍历内存条中地址访问到其他应用的内容,就导致账号被盗等安全问题,所以这个方式也是不安全

  • 怎么解决这些问题呢?工程师们发现加载到内存中的应用,大多数之用到一小部分功能,这就导致了资源的浪费,于是就产生了懒加载。懒加载是引用加载到内存中时,先加载启动相关的,后面要用到就再加载到内存条。

    截屏2021-09-02 15.18.10.png

这样虽然会减少内存的占用,但是应用接下来的内存不知道要分配到哪,这样就导致代码不连续,需要在运行过程不断计算地址很不方便,而且效率很低,于是工程师们就创造了虚拟表,也就引出了虚拟内存

虚拟内存

  • 有了虚拟表,应用程序就只读代码,而代码计算地址的事情交给CPU和硬件MMUMMU内存管理单元,它只做一件事:翻译地址

    Xnip2022-08-01_17-05-45.png

    • 这样应用程序的在运行时访问的内存就是连续的,而访问的内存就是虚拟内存,虚拟内存对应的就是计算好的物理地址。
    • 虚拟地址和物理地址不是一个字节一个字节对印的,这样效率就很低。由于现在的应用内存是一块一块的,干脆就以块为单位去对印,单位就是page,此刻就产生了内存分页管理的概念

映射表(内存分页)

  • 页的大小在不同的操作系统中是不一样的。在iOS(64位)一页是16K,在MAC中是4K。在MAC中可以使用环境变量PAGESIZE查看 Xnip2022-08-01_18-44-06.png

  • 现在内存连续的问题得以解决,安全问题也解决了。由于一个应用只访问自己的虚拟表,而翻译出来的物理地址也是固定的,所以那些外挂就无法访问其它应用。内存溢出的问题也解决了,因为现在内存都是一页一页的访问,所以就不会产生溢出。

  • 当应用加载时,首先启动时需要的代码会直接加到内存,当要执行新的代码时,cpu发现这段代码内存中没有就会把代码卡住,然后操作系统会将这页加到物理内存中,这个现象就叫做缺页中断(pagefault)

  • 操作系统需要执行的一页代码载入到物理内存中时,会往空缺处插入。但手机启动后,物理内存就没有空位了,里面都放了其它的一些数据,但到底往哪里加呢,这个由操作系统决定。操作系统提供一个算法:页面置换算法,它会覆盖掉不那么活跃的部分。所以有时候多打开些应用后,再去进入第一次打开的应用时会重新启动。

  • 有时候在访问内存时会访问比App当前大的内存,因为虚拟表有8G大小,能访问的有4G,那么他会访问到其它应用吗?不会,这块虚拟内存是没有数据,它指向NULL

截屏2021-09-04 23.43.56.png

rebase/binding

  • binding是绑定,当内部文件访问外部函数,就要通过内部符号绑定访问外部,现在的绑定方式都是懒加载

  • rebase是重定位。由于虚拟内存产生后,每次内存都是从0开始,这个时候相应的安全就不存在了,所以操作系统就出现了一个新技术ASLR:让每次生成的虚拟页面,不要从0开始,从随机的值开始,应用的每次启动的起始位置都是随机的。此时行数的位置就成了ASLR+OFFSET,也就是rebase重定向

  • 少数缺页中断几乎是感知不到的,时间是毫秒级别。但同时有大量的缺页中断,比如成百上千甚至更多这个时候用户就能感知到了,冷启动时就会产生大量的缺页异常。

System Trace 查看缺页数

打开 Instruments 选择 System Trace: Xnip2022-08-01_21-12-42.png

选择设备和APP : Xnip2022-08-01_21-20-14.png

启动完成后点击: Xnip2022-08-01_21-22-05.png

点击启动完成,自动解析,解析完成后 搜索 main thread - 选择 Virtual MemoryFile Backed Page In就是缺页信息,可以看到缺页中断的个数以及所用的时间: Xnip2022-08-01_21-47-47.png

通过观察发现消耗的总时间中,PageFault占绝大部份

紧接在再次启动,缺页数和时间都变得很小了,因为此时应用的物理内存还没被覆盖,所以启动会时间会少很多 Xnip2022-08-01_21-52-54.png

  • 冷启动:应用在物理内存中没有占用时的启动是冷启动。例如第一次打开App,或者APP被杀死后,一段时间过后再打开,都是冷启动
  • 热启动:应用编译在物理内存中的内存还存在的启动就是热启动,例如短时间APP从后台返回,APP杀死后立即打开,此时它的物理内存还存在,此时就是热启动。

如果减少PageFault的个数,就会达到优化的目的,具体操作下篇文章再进行讲解