Mach-O
Mach-O 文件是运行时可执行文件类型,包括:
- 可执行文件。Executable — Main binary for application.
- Dylib — Dynamic library.(aka DSO or DLL)
- Bundle — Dylib that can not be linked. 只能在运行时用
dlopen()
函数打开。 - Images — Any executable dylib or bundle.
- Framework — Dylib with directory for resources and headers.
- 图像格式:分成3段,每一段都是页面大小的倍数。
页面大小由硬件决定,arm64处理器页面大小是16K,其他的是4K。
- 通用文件:Universal Files
假设你有一个Mach-O文件运行在64位(arm64)处理器的设备上,如果你想把它运行在32位(armv7s)的设备上,Xcode里会发生什么变化?
答案是:会生成另一个单独的Mach-O文件。然后这两个文件合并生成第三个文件,这个文件就是通用文件。通用文件会有一个头文件,占一页大小。
虚拟内存:Virtual Memory
虚拟内存是间接层。当所有的进程存在时,OS 会使用间接层来管理物理内存。每一个进程都是一个逻辑地址空间,它们被映射到 RAM 的某个物理页面。
Page fault
:如果一个虚拟内存不映射到任何物理内存,那么访问这个进程 时,就会产生页面错误,内核会停止该进程,并试图找出解决方案。- 多个进程可以共享同一块物理内存,进程共享。
File backed pages
:基于文件的映射:不用把整个文件读入 RAM,而是调用mmap()
函数告诉虚拟内存系统,我想把这个文件映射到进程里的这段地址。Copy-On-Write (COW)
:写入时复制。写入时复制所做的就是它积极地在所有进程里共享DATA页面,只要进程只读有共享内容的全局变量,但是一旦有进程想要写入其DATA页面,写入时复制开始。内核会把该页面复制,放入另一个物理RAM并重定向映射,所以该进程有了该页面的副本。Dirty vs. clean Pages
:脏页面和干净页面。上面的副本被认为是脏页面。脏页面是指含有进程特定信息。干净页面是指内核可以按照需要重新建立的页面,比如重新读取磁盘的时候。脏页面比干净页面昂贵得多。
exec() —> main() 的过程
- exec() 是一个系统调用。当你进入一个内核,说:我想把这个进程换成这个新程序。然后内核会抹去整个地址来映射这个新的可执行程序。ASLR 会给它分配一个随机地址。下一步是从该随机地址回溯到 0 地址,把整个地址标记为不可访问:
- Kernal loads helper program
- Dyld (Dynamic loader)
- Executions starts in Dyld (aka LD.SO)
当内核完成内存映射之后,就把指针指向Dyld,让Dyld来完成进程的启动。它的工作是加载所有依赖的Dylib,让它们准备好开始运行。其加载过程如下:
Load dylibs
- 读取所有依赖的 Dylib。首先从内核中读取已经加载好的主可执行文件。在这个主可执行文件的 Header 中有所有依赖库的列表。然后打开和运行这些Dylib,验证它是不是一个 Mach-O 文件,找到它的编码签名,在内核里对它进行注册,在该 Dylib 的每一段调用
mmap()
函数。 - 在加载每个 Dylib时,每个 Dylib 可能还依赖于另一个 Dylib,所以需要递归式的把它们一个一个找出来加载到内存。
- 读取所有依赖的 Dylib。首先从内核中读取已经加载好的主可执行文件。在这个主可执行文件的 Header 中有所有依赖库的列表。然后打开和运行这些Dylib,验证它是不是一个 Mach-O 文件,找到它的编码签名,在内核里对它进行注册,在该 Dylib 的每一段调用
DATA修复
- 重设基址(Rebase):遍历所有内部数据指针,然后为它们添加一个滑动值。这些指针在段里的位置都编码在__LINKEDIT段里。I/O比较多。
- 绑定(Bind):针对那些指向Dylib范围外的指针而言的。其计算复杂度比Rebase要高得多。
- ObjC:经过前两步之后,在ObjC运行时还需要一些额外的操作。在ObjC运行时,必须要维护一张表格,包含所有名称及其映射的类。每次加载的名称都将定义一个类,名称需要登记在一个全局表格里
Initializer
- 跳转到main()函数
启动时间优化
经过上面的总结:
理论上 App 的启动时间是由 main() 函数之前的加载时间(t1)和 main() 函数之后的加载时间(t2)。
t1 时间优化
加入 DYLD_PRINT_STATISTICS 分析 App 启动日志
在环境变量中添加 DYLD_PRINT_STATISTICS
就可以在控制台看见自己 App 的启动时间输出。
优化方向
根据上文的总结,首先就是需要减少动态库的加载,构建OS时,会预计算大量的dylib数据,但是始终无法全部预算。
减少 rebase/binding time。时间都花费在修复__DATA里的指针。所以方法就是减少需要修复的指针。
减少ObjC类对象和ivars的数量。
使用Swift语言 :Swift 通常用的数据要少一些。而且 Swift 更为内联,可以更好地使用 code-gen 减少消耗。
使用 initialize 方法替换 load 方法。
- 显示的初始化器:
ObjC +load()
方法。如果你是用的话最好换成+initialize()
方法,但是这种显示的初始化器最好换成点初始化器:dispatch_once() 或者 pthread_once() 或者 std::once()。 - 隐式初始化器:大部分来自 C++ 的全局变量,带有非默认的初始化器,非默认的构造函数。这里可以用前面提到的点初始化器替代,或者把全局的换成非全局的结构或者指针,指向想要初始化的对象。或者 Only set simple values (PODs)。或者用 Swift 重新编写,Swift 的全局变量可以在使用前确保被初始化,其本质还是在后台调用了点初始化器。
- 显示的初始化器:
t2 时间优化
在 main()
被调用过后,App 主要所做的是一些初始化工作和首页显示。
首先的显示主要包含四个方面:
- 图片的解码
- 控件的布局
- 控件的绘制
- 网络请求
初始化主要是:
- 加载 UserDefault 产生的 plist 文件
didFinishLaunching
中的配置代码
优化方向
- 使用纯代码代替 nib 加载首页视图
- 在控件布局周期中打点观察,特别是
viewDidLoad
和viewWillAppear
这两个操作 - 减少 Log
- 梳理启动的网络请求,评估是否可以在
- 延迟加载
didFinishLaunching
中的某些配置代码