App 运行理论设计模式
main() 执行前发生的事缓存
Mach-O 格式安全
虚拟内存基础数据结构
Mach-O 二进制的加载多线程
理论速成架构
Mach-O 术语app
Mach-O 是针对不一样运行时可执行文件的文件类型。dom
文件类型:ide
Executable: 应用的主要二进制函数
Dylib: 动态连接库(又称 DSO 或 DLL)
Bundle: 不能被连接的 Dylib,只能在运行时使用 dlopen() 加载,可当作 macOS 的插件。
Image: executable,dylib 或 bundle
Framework: 包含 Dylib 以及资源文件和头文件的文件夹
Mach-O 镜像文件
Mach-O 被划分红一些 segement,每一个 segement 又被划分红一些 section。
segment 的名字都是大写的,且空间大小为页的整数。页的大小跟硬件有关,在 arm64 架构一页是 16KB,其他为 4KB。
section 虽然没有整数倍页大小的限制,可是 section 之间不会有重叠。
几乎全部 Mach-O 都包含这三个段(segment): __TEXT,__DATA 和 __LINKEDIT:
__TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。
__DATA 包含全局变量,静态变量等。可读写(rw-)。
__LINKEDIT 包含了加载程序的『元数据』,好比函数的名称和地址。只读(r–)。
Mach-O Universal 文件
FAT 二进制文件,将多种架构的 Mach-O 文件合并而成。它经过 Fat Header 来记录不一样架构在文件中的偏移量,Fat Header 占一页的空间。
按分页来存储这些 segement 和 header 会浪费空间,但这有利于虚拟内存的实现。
虚拟内存
虚拟内存就是一层间接寻址(indirection)。软件工程中有句格言就是任何问题都能经过添加一个间接层来解决。虚拟内存解决的是管理全部进程使用物理 RAM 的问题。经过添加间接层来让每一个进程使用逻辑地址空间,它能够映射到 RAM 上的某个物理页上。这种映射不是一对一的,逻辑地址可能映射不到 RAM 上,也可能有多个逻辑地址映射到同一个物理 RAM 上。针对第一种状况,当进程要存储逻辑地址内容时会触发 page fault;第二种状况就是多进程共享内存。
对于文件能够不用一次性读入整个文件,可使用分页映射(mmap())的方式读取。也就是把文件某个片断映射到进程逻辑内存的某个页上。当某个想要读取的页没有在内存中,就会触发 page fault,内核只会读入那一页,实现文件的懒加载。
也就是说 Mach-O 文件中的 __TEXT 段能够映射到多个进程,并能够懒加载,且进程之间共享内存。__DATA 段是可读写的。这里使用到了 Copy-On-Write 技术,简称 COW。也就是多个进程共享一页内存空间时,一旦有进程要作写操做,它会先将这页内存内容复制一份出来,而后从新映射逻辑地址到新的 RAM 页上。也就是这个进程本身拥有了那页内存的拷贝。这就涉及到了 clean/dirty page 的概念。dirty page 含有进程本身的信息,而 clean page 能够被内核从新生成(从新读磁盘)。因此 dirty page 的代价大于 clean page。
Mach-O 镜像 加载
因此在多个进程加载 Mach-O 镜像时 __TEXT 和 __LINKEDIT 由于只读,都是能够共享内存的。而 __DATA 由于可读写,就会产生 dirty page。当 dyld 执行结束后,__LINKEDIT 就没用了,对应的内存页会被回收。
安全
ASLR(Address Space Layout Randomization):地址空间布局随机化,镜像会在随机的地址上加载。这实际上是一二十年前的旧技术了。
代码签名:可能咱们认为 Xcode 会把整个文件都作加密 hash 并用作数字签名。其实为了在运行时验证 Mach-O 文件的签名,并非每次重复读入整个文件,而是把每页内容都生成一个单独的加密散列值,并存储在 __LINKEDIT 中。这使得文件每页的内容都能及时被校验确并保不被篡改。
从 exec() 到 main()
exec() 是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(由于使用 ASLR)。并将起始位置到0x000000 这段范围的进程权限都标记为不可读写不可执行。若是是 32 位进程,这个范围至少是 4KB;对于 64 位进程则至少是 4GB。NULL 指针引用和指针截断偏差都是会被它捕获。
dyld 加载 dylib 文件
Unix 的前二十年很安逸,由于那时尚未发明动态连接库。有了动态连接库后,一个用于加载连接库的帮助程序被建立。在苹果的平台里是 dyld,其余 Unix 系统也有 ld.so。 当内核完成映射进程的工做后会将名字为 dyld 的Mach-O 文件映射到进程中的随机地址,它将 PC 寄存器设为 dyld 的地址并运行。dyld 在应用进程中运行的工做是加载应用依赖的全部动态连接库,准备好运行所需的一切,它拥有的权限跟应用同样。
下面的步骤构成了 dyld 的时间线:
Load dylibs -> Rebase -> Bind -> ObjC -> Initializers
加载 Dylib
从主执行文件的 header 获取到须要加载的所依赖动态库列表,而 header 早就被内核映射过。而后它须要找到每一个 dylib,而后打开文件读取文件起始位置,确保它是 Mach-O 文件。接着会找到代码签名并将其注册到内核。而后在 dylib 文件的每一个 segment 上调用mmap()。应用所依赖的 dylib 文件可能会再依赖其余 dylib,因此 dyld 所须要加载的是动态库列表一个递归依赖的集合。通常应用会加载 100 到 400 个 dylib 文件,但大部分都是系统 dylib,它们会被预先计算和缓存起来,加载速度很快。
Fix-ups
在加载全部的动态连接库以后,它们只是处在相互独立的状态,须要将它们绑定起来,这就是 Fix-ups。代码签名使得咱们不能修改指令,那样就不能让一个 dylib 的调用另外一个 dylib。这时须要加不少间接层。
现代 code-gen 被叫作动态 PIC(Position Independent Code),意味着代码能够被加载到间接的地址上。当调用发生时,code-gen 实际上会在 __DATA 段中建立一个指向被调用者的指针,而后加载指针并跳转过去。
因此 dyld 作的事情就是修正(fix-up)指针和数据。Fix-up 有两种类型,rebasing 和 binding。
Rebasing 和 Binding
Rebasing:在镜像内部调整指针的指向
Binding:将指针指向镜像外部的内容
能够经过命令行查看 rebase 和 bind 等信息:
1
|
xcrun dyldinfo -rebase -bind -lazy_bind myapp.app/myapp
|
经过这个命令能够查看全部的 Fix-up。rebase,bind,weak_bind,lazy_bind 都存储在 __LINKEDIT 段中,并可经过LC_DYLD_INFO_ONLY 查看各类信息的偏移量和大小。
建议用 MachOView 查看更加方便直观。
从 dyld 源码层面简要介绍下 Rebasing 和 Binding 的流程。
ImageLoader 是一个用于加载可执行文件的基类,它负责连接镜像,但不关心具体文件格式,由于这些都交给子类去实现。每一个可执行文件都会对应一个 ImageLoader 实例。ImageLoaderMachO 是用于加载 Mach-O 格式文件的 ImageLoader 子类,而ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都继承于 ImageLoaderMachO,分别用于加载那些 __LINKEDIT 段为传统格式和压缩格式的 Mach-O 文件。
由于 dylib 之间有依赖关系,因此 ImageLoader 中的好多操做都是沿着依赖链递归操做的,Rebasing 和 Binding 也不例外,分别对应着 recursiveRebase() 和 recursiveBind() 这两个方法。由于是递归,因此会自底向上地分别调用 doRebase() 和 doBind()方法,这样被依赖的 dylib 老是先于依赖它的 dylib 执行 Rebasing 和 Binding。传入 doRebase() 和 doBind() 的参数包含一个LinkContext 上下文,存储了可执行文件的一堆状态和相关的函数。
在 Rebasing 和 Binding 前会判断是否已经 Prebinding。若是已经进行过预绑定(Prebinding),那就不须要 Rebasing 和 Binding 这些 Fix-up 流程了,由于已经在预先绑定的地址加载好了。
ImageLoaderMachO 实例不使用预绑定会有五个缘由:
Mach-O Header 中 MH_PREBOUND 标志位为 0
镜像加载地址有偏移(这个后面会讲到)
依赖的库有变化
镜像使用 flat-namespace,预绑定的一部分会被忽略
LinkContext 的环境变量禁止了预绑定
ImageLoaderMachO 中 doRebase() 作的事情大体以下:
若是使用预绑定,fgImagesWithUsedPrebinding 计数加一,并 return;不然进入第二步
若是 MH_PREBOUND 标志位为 1(也就是能够预绑定但没使用),且镜像在共享内存中,重置上下文中全部的 lazy pointer。(若是镜像在共享内存中,稍后会在 Binding 过程当中绑定,因此无需重置)
若是镜像加载地址偏移量为0,则无需 Rebasing,直接 return;不然进入第四步
调用 rebase() 方法,这才是真正作 Rebasing 工做的方法。若是开启 TEXT_RELOC_SUPPORT 宏,会容许 rebase() 方法对__TEXT 段作写操做来对其进行 Fix-up。因此其实 __TEXT 只读属性并非绝对的。
ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 分别实现了本身的 doRebase() 方法。实现逻辑大同小异,一样会判断是否使用预绑定,并在真正的 Binding 工做时判断 TEXT_RELOC_SUPPORT 宏来决定是否对 __TEXT 段作写操做。最后都会调用setupLazyPointerHandler 在镜像中设置 dyld 的 entry point,放在最后调用是为了让主可执行文件设置好 __dyld 或__program_vars。
Rebasing
在过去,会把 dylib 加载到指定地址,全部指针和数据对于代码来讲都是对的,dyld 就无需作任何 fix-up 了。现在用了 ASLR 后悔将 dylib 加载到新的随机地址(actual_address),这个随机的地址跟代码和数据指向的旧地址(preferred_address)会有误差,dyld 须要修正这个误差(slide),作法就是将 dylib 内部的指针地址都加上这个偏移量,偏移量的计算方法以下:
Slide = actual_address - preferred_address
而后就是重复不断地对 __DATA 段中须要 rebase 的指针加上这个偏移量。这就又涉及到 page fault 和 COW。这可能会产生 I/O 瓶颈,但由于 rebase 的顺序是按地址排列的,因此从内核的角度来看这是个有次序的任务,它会预先读入数据,减小 I/O 消耗。
Binding
Binding 是处理那些指向 dylib 外部的指针,它们实际上被符号(symbol)名称绑定,也就是个字符串。以前提到 __LINKEDIT 段中也存储了须要 bind 的指针,以及指针须要指向的符号。dyld 须要找到 symbol 对应的实现,这须要不少计算,去符号表里查找。找到后会将内容存储到 __DATA 段中的那个指针中。Binding 看起来计算量比 Rebasing 更大,但其实须要的 I/O 操做不多,由于以前 Rebasing 已经替 Binding 作过了。
ObjC Runtime
Objective-C 中有不少数据结构都是靠 Rebasing 和 Binding 来修正(fix-up)的,好比 Class 中指向超类的指针和指向方法的指针。
ObjC 是个动态语言,能够用类的名字来实例化一个类的对象。这意味着 ObjC Runtime 须要维护一张映射类名与类的全局表。当加载一个 dylib 时,其定义的全部的类都须要被注册到这个全局表中。
C++ 中有个问题叫作易碎的基类(fragile base class)。ObjC 就没有这个问题,由于会在加载时经过 fix-up 动态类中改变实例变量的偏移量。
在 ObjC 中能够经过定义类别(Category)的方式改变一个类的方法。有时你想要添加方法的类在另外一个 dylib 中,而不在你的镜像中(也就是对系统或别人的类动刀),这时也须要作些 fix-up。
ObjC 中的 selector 必须是惟一的。
Initializers
C++ 会为静态建立的对象生成初始化器。而在 ObjC 中有个叫 +load 的方法,然而它被废弃了,如今建议使用 +initialize。对比详见:http://stackoverflow.com/questions/13326435/nsobject-load-and-initialize-what-do-they-do
如今有了主执行文件,一堆 dylib,其依赖关系构成了一张巨大的有向图,那么执行初始化器的顺序是什么?自顶向上!按照依赖关系,先加载叶子节点,而后逐步向上加载中间节点,直至最后加载根节点。这种加载顺序确保了安全性,加载某个 dylib 前,其所依赖的其他 dylib 文件确定已经被预先加载。
最后 dyld 会调用 main() 函数。main() 会调用 UIApplicationMain()。
改善启动时间
从点击 App 图标到加载 App 闪屏之间会有个动画,咱们但愿 App 启动速度比这个动画更快。虽然不一样设备上 App 启动速度不同,但启动时间最好控制在 400ms。须要注意的是启动时间一旦超过 20s,系统会认为发生了死循环并杀掉 App 进程。固然启动时间最好以 App 所支持的最低配置设备为准。直到 applicationWillFinishLaunching 被调动,App 才启动结束。
测量启动时间
Warm launch: App 和数据已经在内存中
Cold launch: App 不在内核缓冲存储器中
冷启动(Cold launch)耗时才是咱们须要测量的重要数据,为了准确测量冷启动耗时,测量前须要重启设备。在 main() 方法执行前测量是很难的,好在 dyld 提供了内建的测量方法:在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量DYLD_PRINT_STATISTICS 设为 1。控制台输出的内容以下:
1
2
3
4
5
6
7
8
|
Total pre-main time: 228.41 milliseconds (100.0%)
dylib loading time: 82.35 milliseconds (36.0%)
rebase/binding time: 6.12 milliseconds (2.6%)
ObjC setup time: 7.82 milliseconds (3.4%)
initializer time: 132.02 milliseconds (57.8%)
slowest intializers :
libSystem.B.dylib : 122.07 milliseconds (53.4%)
CoreFoundation : 5.59 milliseconds (2.4%)
|
优化启动时间
能够针对 App 启动前的每一个步骤进行相应的优化工做。
加载 Dylib
以前提到过加载系统的 dylib 很快,由于有优化。但加载内嵌(embedded)的 dylib 文件很占时间,因此尽量把多个内嵌 dylib 合并成一个来加载,或者使用 static archive。使用 dlopen() 来在运行时懒加载是不建议的,这么作可能会带来一些问题,而且总的开销更大。
Rebase/Binding
以前提过 Rebaing 消耗了大量时间在 I/O 上,而在以后的 Binding 就不怎么须要 I/O 了,而是将时间耗费在计算上。因此这两个步骤的耗时是混在一块儿的。
以前说过能够从查看 __DATA 段中须要修正(fix-up)的指针,因此减小指针数量才会减小这部分工做的耗时。对于 ObjC 来讲就是减小 Class,selector 和 category 这些元数据的数量。从编码原则和设计模式之类的理论都会鼓励你们多写精致短小的类和方法,并将每部分方法独立出一个类别,其实这会增长启动时间。对于 C++ 来讲须要减小虚方法,由于虚方法会建立 vtable,这也会在__DATA 段中建立结构。虽然 C++ 虚方法对启动耗时的增长要比 ObjC 元数据要少,但依然不可忽视。最后推荐使用 Swift 结构体,它须要 fix-up 的内容较少。
ObjC Setup
针对这步所能事情不多,几乎都靠 Rebasing 和 Binding 步骤中减小所需 fix-up 内容。由于前面的工做也会使得这步耗时减小。
Initializer
显式初始化
使用 +initialize 来替代 +load
不要使用 __atribute__((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。好比使用dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用时才初始化,推迟了一部分工做耗时。
隐式初始化
对于带有复杂(non-trivial)构造器的 C++ 静态变量:
在调用的地方使用初始化器。
只用简单值类型赋值(POD:Plain Old Data),这样静态连接器会预先计算 __DATA 中的数据,无需再进行 fix-up 工做。
使用编译器 warning 标志 -Wglobal-constructors 来发现隐式初始化代码。
使用 Swift 重写代码,由于 Swift 已经预先处理好了,强力推荐。
不要在初始化方法中调用 dlopen(),对性能有影响。由于 dyld 在 App 开始前运行,因为此时是单线程运行因此系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这就严重影响了性能,还可能会形成死锁以及产生未知的后果。因此也不要在初始化器中建立线程。
Reference:https://developer.apple.com/videos/play/wwdc2016/406/