优化应用的启动时间(理论篇)
这是 WWDC 2016 Session 406 理论部分的笔记,内容包含着了 Mach-O,虚拟内存的一点点知识,不过主要还是关注在 main()
函数之前做了什么。
Mach-O
Mach-O 是一种运行时可执行二进制文件类型。除了在应用中常见的可执行文件之外,还有 dylib(动态库),bundle(特殊的 dylib,只能在运行时通过 dlopen()
函数装载)等等,都是 Mach-O 文件,也被称作 Image。这些文件运行在那些基于 Mach 内核的操作系统上,比如 macOS 和 iOS 等等。
Mach-O 文件的结构
Mach-O_Format.pngMach-O 被分为不同的 segment,每个 segment 的大小都是页面大小(page size)的倍数(arm64 环境下 page size = 16KB,其它为 4K)。常见的有 __TEXT, __DATA, __LINKEDIT。通过 otool -tV -d
可以读取对象文件的 __TEXT 段和 __DATA 段的内容:
- __TEXT segment 包含 Mach 头文件、代码以及只读常量;
- __DATA segment 包含有可读的内容,如全局变量、静态变量等等;
- __LINKEDIT segment 包含有如何装载程序的元数据(meta data)。
Universal Binary
Univseral_Files.pngUniversal Binary 最早在 WWDC 2005 上被提出,目的是帮助 OS X 上的应用从基于 PowerPC 架构到 Intel 架构的转变。即可执行文件中包含着多种指令集,不同的系统可以根据 Mach-O 上的 Fat Header 上的信息选择执行相应的指令,副作用是使得可执行文件的体积增大。
比如将 Build Setting -> Valid Architectures 中的 armv7 和 armv7s 删除,仅剩 arm64,那么导出的可执行文件的体积会小很多(除非应用仅支持64位处理器,否则不要这么做)。
Size_Cmp_Diff_Arch.png虚拟内存
虚拟内存是一种将进程虚拟地址空间映射到物理地址的一种机制。使用虚拟地址的好处是使得程序空间独立。一般的操作系统在实现虚拟内存的时候,都会将内存空间划分为页(page),通过页表(page table)去管理虚拟页和物理页之间的映射关系和 page-in & page-out 。
ASLR
地址空间布局随机化(Address Space Layout Randomization),即将可执行文件、动态库等文件随机地装载到内存的某个地址中防止缓冲区溢出攻击。
Code Signing
每一个页的内容都被加密散列,散列值存放在 __LINKEDIT segment 中。在 page-in 的时候会检验内容的正确性,这意味着程序的指令不会被修改。
从 exec() 到 main()
在执行 main()
函数之前,大概经过了这些步骤:
- 运行辅助程序 dyld(与进程在同一地址空间);
- dyld 装载所有程序依赖的动态库;
- dyld 修正 __DATA segment 内的数据指针;
- 调用所有 initializers.
Loading Dylibs :
动态库(或者叫共享库,通常是 .so .dylib 作为后缀)是一种可以在程序在运行时装载的目标文件。使用动态库可以有效减少代码的体积,提高代码的复用率。下面是 iOS 在运行前加载动态库的过程:
- 解析程序所依赖的 dylibs;
- 找到所需的 Mach-O 文件;
- 打开并读取文件;
- 校验 Mach-O 文件;
- 向内核注册代码签名;
- 对每个 segment 调用
mmap()
,将目标文件映射到内存中。
上面的这个过程是递归进行的,因为一个动态库可能还依赖着另一个动态库。
Rebasing
Rebasing 的工作是改变 dylibs 或者 bundles 的基地址。基地址是 image(dylib 或者 bundle)在被装载时优先选择的地址,被编码到 __LINKEDIT 中,默认为零。在运行时,如果基地址范围被占用了,那么 dyld 会将这个 image 装载到一个新的地址空间去(内容来源于 rebase 的 manual,在 terminal 里敲入 man rebase
就能看到)。
Binding
不同于 rebasing(修正 dylib 内部每个指向 image 内部的地址),binding 是要修正所有指向其它 dylib 指针的值。比如说在程序中调用 malloc 函数,dyld 需要在共享库中找到 malloc 这个符号对应的子程序的地址然后修改调用程序中的指针。
这里有个命令可以查看可执行文件中的 dyld 信息:
xcrun dyldinfo -rebase -bind -lazy_bind YourExecutableFile
Notify ObjC Runtime
在 rebasing 和 binding 结束之后,ObjC runtime 初始化,接着通过 class_createInstance()
注册类到 runtime 中。类的成员变量的偏移量随之更新,category 上的方法也被插入到方法列表中。
Initializers
之后调用 ObjC 中类的 +load
方法。视频中 Nick Kledzik 说 +load
这个方法已经 deprecated 了,不建议使用,但是去 NSObject Class Reference 看,并没有对此做了什么标记。接着调用 __attribute__((constructor))
修饰的函数,然后就是对 C++ 中全局对象进行初始化(如下图,Person
的构造函数先于程序的 main()
函数的调用)。
结束
至此可知,在 main()
函数之前,我们可以通过减少库的数目、减少类的数目、不在+load
里面做过多的事情等手段,加快应用的启动速度。在 main()
函数之后,就是要求主线程在 -application:didFinishLaunchingWithOptions:
中尽快返回。
上述的每一个步骤都可以展开很多内容来讲,这里推荐一些比较厉害的博客,sunnyxx 的《iOS 程序 main 函数之前发生了什么》 还有 mikeash.com 上的《Friday Q&A 2012-11-09: dyld: Dynamic Linking On OS X》。
我才不会说我把这个视频的字幕翻译了一遍才勉强看得懂咧……🙈