iOS App的启动过程

一、mach-O

  • Executable 可执行文件
  • Dylib 动态库
  • Bundle 无法被连接的动态库,只能通过 dlopen() 加载
  • Image 指的是 Executable,Dylib 或者 Bundle 的一种。
  • Framework 动态库和对应的头文件和资源文件的集合

Apple 出品的操作系统的可执行文件格式几乎都是 mach-O。

mach-O 可以大致分为三部分:


  • Header 头部,包含可以执行的 CPU 架构,比如 x86,arm64
  • Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
  • Data 数据,包含 load commands 中需要的各个段(segment)的数据,每个 Segment 的大小都是 Page 的整数倍。

用 MachOView 打开 Demo 工程的可以执行文件,来验证下 mach-O 的文件布局:


图中分析的 mach-O 文件来源于 PullToRefreshKit。这是一个纯 Swift 的编写的工程。

那么 Data 部分又包含那些 segment 呢?绝大多数 mach-O 包括以下三个阶段(支持用户自定义Segment,但是很少使用)

  • __TEXT 代码段,只读。包含函数和只读的字符串,上图中类似 __TEXT,__text 的都是代码段
  • __Data 数据段,读写。包括可读写的全局变量等,__DATA,__data 都是数据段
  • __LINKEDIT 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。

关于 mach-O 更多细节,可以看看文档:《Mac OS X ABI Mach-O File Format Reference》

二、dyld

dyld 的全称是 dynamic loader,它的作用是加载一个进程所需要的 image,dyld 是开源的

三、Virtual Memory

虚拟内存是在物理内存上建立的一个逻辑地址空间,它向上(应用)提供了一个连续的逻辑地址空间,向下隐藏了物理内存的细节。
虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应到一个物理地址。
虚拟内存被划分为一个个大小相同的 Page(64 位系统上是 16KB),提高管理和读写的效率。 Page 又分为只读和读写的 Page。

虚拟内存是建立在物理内存和进程之间的中间层。在 iOS 上,当内存不足的时候,会尝试释放那些只读的 Page,因为只读的 Page 在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的App(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台的App。

四、Page fault

在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑 Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次 Page fault。当 Page fault 发生的时候,会中断当前的程序,在物理内存中寻找一个可用的 Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。

五、Dirty Page & Clean Page

  • 如果一个 Page 可以从磁盘上重新生成,那么这 Page 称为 Clear Page
  • 如果一个 Page 包含了进程相关信息,那么这个 Page 称为 Dirty Page

像代码段这种只读的 Page 就是 Clean Page。而数据段(__DATA)这种读写的 Page,当写数据发生的时候,会触发 CO(Copy on write),也就是写时复制,Page 会被标记成 Dirty,同时会被复制。

想要了解更多细节,可以阅读文档:Memory Usage Performance Guidelines

六、启动过程

使用 dyld2 启动应用的过程如图:


大致的过程如下:

  • 加载 dyld 到 App 进程
  • 加载动态库(包括所依赖的所有动态库)
  • Rebase
  • Bind
  • 初始化 Objective-C Runtime
  • 其它的初始化代码

6.1 加载动态库

dyld 会首先读取 mach-O 文件的 Header 和 load commands。

接着就知道了这个可执行文件依赖的动态库。例如加载动态库 A 到内存,接着检查 A 所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个 App 所依赖的动态库在 100~400 个左右,其中大多数都是系统的动态库,它们会被缓存到 dyld shared cache,这样读取的效率会很高。

查看 mach-O 文件所依赖的动态库,可以通过 MachOView 的图形化界面(展开 Load Command 就能看到),也可以通过命令行 otool。

$ otool -L demo 
demo:
    @rpath/PullToRefreshKit.framework/PullToRefreshKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1444.12.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    @rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 900.0.65)
    @rpath/libswiftCoreAudio.dylib (compatibility version 1.0.0, current version 900.0.65)
    //...

6.2 Rebase && Bind

先讲讲为什么要 Rebase。

有两种主要的技术来保证应用的安全:ASLRCode Sign

ASLR 的全称是 Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而 ASLR 技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址 + 偏移量找到函数的地址。

Code Sign 相信大多数开发者都知晓,这里要提一点的是,在进行 Code sign 的时候,加密哈希不是针对于整个文件,而是针对于每一个 Page 的。这就保证了在 dyld 进行加载的时候,可以对每一个 page 进行独立的验证。

mach-O 中有很多符号,有指向当前 mach-O 的,也有指向其他 dylib 的,比如 printf。那么,在运行时,代码如何准确的找到 printf 的地址呢?

mach-O 中采用了 PIC 技术,全称是 Position Independ code。当你的程序要调用 printf 的时候,会先在 __DATA 段中建立一个指针指向 printf,在通过这个指针实现间接调用。dyld 这时候需要做一些 fix-up 工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分

  • Rebase 修正内部(指向当前 mach-O 文件)的指针指向
  • Bind 修正外部指针指向


之所以需要 Rebase,是因为刚刚提到的 ASLR 使得地址随机化,导致起始地址不固定,另外由于 Code Sign,导致不能直接修改 Image。Rebase 的时候只需要增加对应的偏移量即可。待 Rebase 的数据都存放在 __LINKEDIT 中。

可以通过 MachOView 查看:Dynamic Loader Info -> Rebase Info

$ xcrun dyldinfo -bind demo 
bind information:
segment section          address        type    addend dylib            symbol
__DATA  __got            0x10003C038    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC4LeftC9textLabelSo7UILabelCvWvd
__DATA  __got            0x10003C040    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC5RightC9textLabelSo7UILabelCvWvd
__DATA  __got            0x10003C048    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6FooterC9textLabelSo7UILabelCvWvd
__DATA  __got            0x10003C050    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6HeaderC7spinnerSo23UIActivityIndicatorViewCvWvd
//...

Rebase 解决了内部的符号引用问题,而外部的符号引用则是由 Bind 解决。在解决 Bind 的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于 Rebase 来说是略慢的。

同样,也可以通过 xcrun dyldinfo 来查看 Bind 的信息,比如我们查看 bind 信息中,包含 UITableView 的部分:

$ xcrun dyldinfo -bind demo | grep UITableView
__DATA  __objc_classrefs 0x100041940    pointer      0 UIKit            _OBJC_CLASS_$_UITableView
__DATA  __objc_classrefs 0x1000418B0    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewCell
__DATA  __objc_data      0x100041AC0    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __objc_data      0x100041BE8    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __objc_data      0x100042348    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __objc_data      0x100042718    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __data           0x100042998    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController
__DATA  __data           0x100042A28    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController
__DATA  __data           0x100042F10    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController
__DATA  __data           0x1000431A8    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController

6.3 Objective-C

Objective-C 是动态语言,所以在执行 main 函数之前,需要把类的信息注册到一个全局的 Table 中。同时,Objective-C 支持 Category,在初始化的时候,也会把 Category 中的方法注册到对应的类中,同时会唯一 Selector,这也是为什么当你的 Cagegory 实现了类中同名的方法后,类中的方法会被覆盖。

另外,由于 iOS 开发是基于 Cocoa Touch 的,所以绝大多数的类起始都是系统类,所以大多数的 Runtime 初始化起始在 Rebase 和 Bind 中已经完成。

6.4 Initializers

接下来就是必要的初始化部分了,主要包括几部分:

  • +load方法。
  • C/C++ 静态初始化对象和标记为 attribute(constructor) 的方法

+load 方法已经被弃用了,如果你用 Swift 开发,你会发现根本无法去写这样一个方法,官方的建议是使用 initialize。区别就是,load 是在类装载的时候执行,initialize 是在类第一次收到 message 前调用。

6.5 dyld3

上面讲解是 dyld2 的加载方式。而最新的是 dyld3 加载方式略有不同:


dyld2 是纯粹的 in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时 dyld2 才能开始执行任务。

dyld3 则是部分 out-of-process,部分 in-process。图中,虚线之上的部分是 out-of-process的,在 App 下载安装和版本更新的时候会去执行,out-of-process 会做如下事情:

  • 分析 Mach-o Headers
  • 分析依赖的动态库
  • 查找需要 Rebase & Bind 之类的符号
  • 把上述结果写入缓存

这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。

文章

为自己丶拼个未来 - 深入理解iOS App的启动过程

posted @ 2020-02-26 13:23  和风细羽  阅读(743)  评论(0编辑  收藏  举报