iOS启动原理(一)

背景



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()函数之前的时间也是启动的一部分。往往这部分时间也不短,所以不能掉以轻心哦~

    作者:小笨狼

    链接:http://www.jianshu.com/p/4fe773d6da4c#comments



    更多推荐:

  • iOS组件化

  • 格物致知iOS类与对象

  • 轻量级低风险 iOS热更新方案

  • iOS启动原理(一)