背景
iOS的启动过程一直比较神秘,这方面的资料也不是太多,大多数的资料都来自2016年WWDC的一篇视频,本文的大部分内容来自于视频,算是视频的一个归纳总结再加上自己的一点点感悟吧。
启动的过程
dyld是App的启动器,启动的大部分事情都由dyld完成,iOS的启动大致分为几个部分:
内核将App的执行文件加载到随机地址空间(加载到随机地址主要是因为ASLR技术)
内核将dyld的执行文件加载到随机地址空间
内核执行dyld文件
dyld启动App
dyld加载所有App所依赖的dylibs(动态库)
执行rebasing/binding修复地址
Objc Setup
initialize
dyld调用App中的main(),将主动权交还给App
手机内核只负责将App的执行文件和dyld加载到内存中,然后所有的启动工作都交给了dyld。
dyld加载App依赖的dylibs
dyld拿到App的执行文件后,首先从文件的header中解析出App依赖的dylib列表,找到每一个依赖的dylib。打开并读取dylib文件的起始位置,验证签名,确保dylib没有被篡改。验证签名后,对dylib中的每个segment调用mmap()
segment
一般每个Mach-O文件都会有三个segment:
__TEXT: 一般处于文件的头部位置,包含Mach header,被执行的代码,和只读常量,只读可执行(r-x)。由于不会被更改,所以读到内存中后可复用
__DATA: 包含各种变量,可读写(rw-),由于可以被更改,所以不可复用
__LINKEDIT: 包含函数名称和对应的地址,只读(r--)
mmap()
文件读入内存并不用一次性读入整个文件,它可以使用分页映射(mmap())的方式进行读取。也就是用到哪个segment,再将哪个segment读入内存,实现文件读入的懒加载。
同时同一个Mach-O文件中的segment也可以映射到多个进程,实现进程之间的内存共享。__TEXT和__LINKEDIT段都是只读的,不会有进程对它进行修改,它们是可以让所有进程共享的,大家都使用同一份内容。然而__DATA段却不是这样,__DATA是可读写的,当某一个进程需要对它进行修改时,需要先copy一份出来,映射到新的 RAM 页上。让这个进程拥有自己独立的内存拷贝,进行修改。这就是Copy-On-Write技术,简称COW。
由于__TEXT和__LINKEDIT段可以进程间共享,只需要在第一次使用的时候进行IO操作,后续即可直接使用,所以App在第一次启动时会比较费时,因为所有的segment读取都需要进行IO操作。后续启动,会快很多,很多segment已经映射到内存中,会被缓存起来,二次启动直接使用,不需要进行IO操作,这就有了iOS中冷启动和热启动的概念:
冷启动:新安装App或者手机重启后,第一次启动。手机需要加载所有的segment
热启动:启动过App后,再次启动。内存中缓存的segment可以直接复用。
执行rebasing/binding修复地址
由于App和每个dylib加载到的都是随机地址空间,代码中原来的函数地址跟真实的函数地址会有差异。修复这个差异的过程就是rebasing和binding。其中rebasing主要做的是image内部的修复,binding主要做的是image间的修复。
Rebasing
对于Image内部的函数,假设它的原地址是A,对应当前地址空间下的新地址是B。那么它所有的函数指针都需要加上地址差(B-A)。所有的Rebasing过程就是从__LINKEDIT取出函数指针,修改函数指针,存入__DATA中,供函数调用。(原始的函数指针存在__LINKEDIT中,修改后的数据存在__DATA中)
之前说到,加载文件使用的是mmap技术,__LINKEDIT和DATA段是在第一次使用时才会执行IO操作,加载到内存中。所以Rebasing阶段,耗时主要是在IO操作上。
Binding
image间的函数指针,实际是被符号名称绑定的,为了找到对应的函数实现,dyld需要去符号表中根据符号名称查找,找到后将地址存到__DATA中对应函数指针中。由于IO操作在rebasing阶段已经在做了,所以binding阶段主要耗时在符号表查找的这个过程,这个过程的主要瓶颈在CPU计算上。
Objc Setup
Objc是一门动态语言,为了维持它的动态性,在启动时,需要将类的名称和类的方法都注册起来。Objc Setup阶段,主要是做Class的注册,Method的注册和Category的注册。
一个好的设计模式,一般都推崇写很多类,每个类尽量简单,写很多Category,每个Category都只包含独立模块的方法。但是从启动速度的角度来说,尽量减少类,Category和方法,才会让Objc Setup阶段耗时更少。
initialize
当所有的Class和method都注册过后,系统需要做一些初始化的工作,对于Objective-C而言,主要是需要调用各个类的+load方法,所以项目中应该尽量避免使用+load方法,正常的初始化工作,可以在initialize中实现。StackOverflow上有详细的关于+load和initialize的对比
End
当上面所有阶段执行完成之后,dyld会调用main()函数,将主动权交还给App。之后才会调用到didFinishLaunch中的代码。
上面介绍的启动时间主要是main()函数之前的启动时间,正常这个时间控制在400ms以内就可以算一个启动速度优异的App了。正常我们关注更多的可能是main()函数后didFinishLaunch中代码的执行时间。但是对用户而言,main()函数之前的时间也是启动的一部分。往往这部分时间也不短,所以不能掉以轻心哦~
作者:小笨狼
链接:https://www.jianshu.com/p/4fe773d6da4c#comments