App启动之Dyld在做什么

4,844 阅读4分钟

前言

这里主要剖析一下一个App从点击图标,到展现首页的整个过程。

App是如何启动的

按顺序划分

  • 加载可执行文件(读取Mach-O)
  • 加载动态库(Dylib)
  • Rebase & Bind
  • Objc
  • Initializers

--------- main() ---------

  • 执行AppDelegate的代理方法(如:didFinishLaunchingWithOptions)。
  • 根据业务注册SDK,获取数据库数据等。
  • 初始化Windows,初始化ViewController。

加载可执行文件(读取Mach-O)

Apple操作系统使用dyld加载可执行文件。

dyld全程为dynomic loader,作用是加载一个进程所需要的Image,在opensource-apple可以找到它的开源代码。

加载动态库(Dylib)

dyld读取完Mach-O的Header和Load Commands后,就会找到可执行文件的依赖动态库。接着dyld会将所依赖的动态库加载到内存中。这是一个递归的过程,依赖的动态库可能还会依赖别的动态库,所以dyld会递归每个动态库,直至所有的依赖库都被加载完毕。

加载后的动态库会被缓存到dyld shared cache中,提高读取效率。

简单的说下Mach-O,简单的可以分为三个部分,Header,Load Commands,Segment Data。

Header中包含的是可执行文件的CPU架构,Load Commands的数量和占用空间。

Load Commands中包含的是Segment的Header与内存分布,以及依赖动态库的版本和Path等。

Segment Data就是Segment汇编代码的实现,每段Segment的内存占用大小都是分页页数的整数倍。

iOS-dyld-2019-03-17-3

Rebase & Bind

这两个过程合在一起说,是因为他们之间的工作是相互补充的。

Apple为了解决应用安全,用到了ASLR(Address space layout randomization 地址空间布局随机化)和Code Sign。

App被启动后,会被映射到虚拟内存中,这样App在这个空间中就有了一个起始地址,但这个起始地址是固定的。ASLR能使这个起始地址随机化,这项技术可以防止攻击者通过初始地址+偏移量的方法找到函数的内存地址。

Code Sign就是签名,在进行加密的时候,会对每一个Page(这里指的是Segment Data)都进行加密,当dyld进行加载的时候,会对每一个Page都进行独立的验证。

Mach-O中采用了PIC(Position Independent Code 地址无关代码),大当我们在调用函数时,会在__Data段中建立一个指向该函数的指针,通过这个指针来间接调用。

Mach-O中有很多符号,有些指向当前Mach-O的(我们为App编写的代码),有些指向其他DyLib(依赖的动态库)。

Rebase的作用是重新修正指向当前Mach-O指针的指向,因为上面提到的ASLR将地址随机化,起始地址不在是固定的,重新修复后,App才能正常运行。

Bind的作用是重新修复外部指针的指向,这个过程会根据字符串匹配的方式来查找符号表,比起Rebase会略慢(这里fishhook的实现基础,它在dyld绑定C库的时候进行了hook)。

iOS-dyld-2019-03-17-2

Objc

因为Objective C的动态特性,所以在Main函数执行之前,需要把类信息注册到一个全局Table中。同时,Category的方法也会被注册到对应类中,Category中的同名方法实现,会根据编译顺序,被最后一个编译的Category实现所覆盖。同时还会做Selector的唯一性检测。

Initializers

这个阶段是包含必要的初始化。

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

这里区分下+load方法与+Initialize方法,前者是在类加载时调用的,后者是在类第一次收到message之前调用的。

main()方法之后的事

这里就不做展开了,都是我们亲手写的代码。

Dyld 3

以上我们介绍了dyld2的加载方式,在2017WWDC,Apple推出了Dyld3。

Dyld2是从程序开始时才开始执行的,而Dyld3则将Dyld2的一些过程进行了分解。

iOS-dyld-2019-03-17-1

Dyld3分为out-of-process,和in-process。

out-process会做:

  • 分析Mach-O Headers
  • 分析以来的动态库
  • 查找需要的Rebase和Bind的符号
  • 将上面的分析结果写入缓存。

in-process会做:

  • 读取缓存的分析结果
  • 验证分析结果
  • 加载Mach-O文件
  • Rebase&Bind
  • Initializers

使用了Dyld3后,App的启动速度会进一步提高

启动阶段的优化建议

  • 减少动态库的数量,推荐使用系统库。
  • 减少类和方法的数量。
  • 减少初始化函数。
  • 尽量使用Swift。
    • Swift没有初始化器。
    • Swift不允许特定类型的未对齐数据结构。
    • Swift代码更精简。

想要了解Dyld的同学,可以看看这篇文章App 启动流程以及优化 WWDC 2017

参考资料

深入理解iOS App的启动过程

App 启动流程以及优化 WWDC 2017