1、Fork 新进程
2、为Mach-o 分配内存
3、解析Mach-o
4、读取Mach-o 头信息
5、遍历load command信息,将Mach-o 映射到内存
6、启动dyld
Apple 出品的操作系统的可执行文件格式几乎都是mach-o

Header 头部,包含可以执行的CPU架构,比如X86,arm64
Load commands 加载命令,包含文件的组织架构和虚拟内存中的布局方式
Data, 数据,包含load commands 中需要的各个段segment的数据,每一个segment的大小都是page的整数倍。
用MachOView 打开Demo工程的可执行文件

Data 部分包含的Segment
__TEXT 代码段,只读,包含函数和只读的字符串
—DATA 数据段,读写,包括可读写的全局变量等
—LINKEDIT 包含了方法和变量的元数据(位置,偏移量)以及代码签名等信息
dyld
dyld 全称dynamic loader,它的作用加载一个进程所需要的image,dyld是开源的 开源地址
Virtual Memory
虚拟内存是建立在物理内存和进程之间的中间层。在iOS上,当内存不足的时候,会尝试释放那些只读的page,因为只读的Page在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的APP(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台App。
Page fault
在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次Page fault。当Page fault发生的时候,会中断当前的程序,在物理内存中寻找一个可用的Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。
Dirty Page & Clean Page
如果一个Page可以从磁盘上重新生成,那么这个Page称为Clean Page
如果一个Page包含了进程相关信息,那么这个Page称为Dirty Page
像代码段这种只读的Page就是Clean Page。而想数据段(_DATA)这种读写的Page,当写数据发生的时候,会触发COW(Copy on write), 也就是写时复制,Page会被标记成Dirty,同时会被复制。
启动过程

大致过程:
1、加载Dyld到APP进程
2、加载动态库(包括所依赖的所有动态库)
3、Rebase
4、Bind
5、初始化Objective C Runtime
6、其他初始化代码
加载动态库
dyld会首先读取mach-o文件的Header和load commands。
接着就知道了这个可执行文件依赖的动态库。例如加载动态库A到内存,接着检查A所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个App所依赖的动态库在100-400个左右,其中大多数都是系统的动态库,它们会被缓存到dyld shared cache,这样读取的效率会很高。
Rebase && Bind
有两种主要的技术来保证应用的安全:ASLR 和Code Sign
ASLPR (Address space layout randomization)地址空间布局随机化。APP被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么很容易通过起始地址+偏移量找到函数的地址
Code Sign加密哈希不是针对于整个文件,而是针对于每个Page的。保证了dyld进行加载的时候,可以对每一个Page进行独立的验证
Mach-o 中有很多符号,指向当前的mach-o的,也有指向其他的dylib的,比如prinf。那么在运行时,代码如何准确的找到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
也可以通过命令行查看:
—————————————————————————————————————————
192:Desktop Leo$ xcrun dyldinfo -rebase demo
bind information:
segment section address type addend dylib symbol
__DATA __got 0x10003C038 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC4LeftC9textLabelSo7UILabelCvWvd
__DATA __got 0x10003C040 pointer 0 PullToRefreshKit __T016PullToRefreshKit07DefaultC5RightC9textLabelSo7UILabelCvWvd
—————————————————————————————————————————
Rebase 解决了内部的符号引用问题,而外部的符号引用则是Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表。所以这个过程相对于Rebase来说略慢
同样,也可以通过xcrun dyldinfo来查看Bind的信息,比如我们查看bind信息中,包含UITableView的部分:
————————————————————————————————————————————————
192:Desktop Leo_UITableView
__DATA __objc_classrefs 0x1000418B0 pointer 0 UIKit OBJC_CLASS_UITableViewController
__DATA __objc_data 0x100041BE8 pointer 0 UIKit OBJC_CLASS$_UITableViewController
————————————————————————————————————————————————
Objective C
Objective C是动态语言,所以在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会唯一Selector,这也是为什么当你的Cagegory实现了类中同名的方法后,类中的方法会被覆盖。
另外,由于iOS开发时基于Cocoa Touch的,所以绝大多数的类起始都是系统类,所以大多数的Runtime初始化起始在Rebase和Bind中已经完成。
Initializers
接下来就是必要的初始化部分了,主要包括几部分:
+load方法。
C/C++静态初始化对象和标记为attribute(constructor)的方法
这里要提一点的就是,+load方法已经被弃用了,如果你用Swift开发,你会发现根本无法去写这样一个方法,官方的建议是实用initialize。区别就是,load是在类装载的时候执行,而initialize是在类第一次收到message前调用。
Dyld3

dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。
dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情:
分析Mach-o Headers
分析依赖的动态库
查找需要Rebase & Bind之类的符号
把上述结果写入缓存
这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。
在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS。

小结:
不同的App在启动的时候做的事情往往不同,但是优化起来的核心思想无非就两个:
能延迟执行的就延迟执行。比如SDK的初始化,界面的创建。
不能延迟执行的,尽量放到后台执行。比如数据读取,原始JSON数据转对象,日志发送。
Main 函数之前
dylibs
启动第一步是加载动态库,加载系统的动态库很快,因为可以缓存,而加载内嵌的动态库速度较慢。所以提高这一步的关键是:减少动态库的数量。
-合并动态库。比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度
Rebase & Bind & Objective C Runtime
Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:
1、减少__DATA段中的指针数量。
2、合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个
3、删除无用的方法和类。
Initializers
减少atribute((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。
load 和 initialize 详解
网友评论