前言
这里将剖析一个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的内存占用大小都是分页页数的整数倍。
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)。
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的一些过程进行了分解。
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