App 启动时在执行 main() 之前做了什么
编译的几个主要过程,生成可执行文件
- 首先,你写好代码后,
LLVM
会预处理你的代码,比如把宏嵌入到对应的位置 - 预处理完后,
LLVM
会对代码进行词法分析和语法分析,生成AST
。AST
是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用AST
能够更快速地进行静态检查,同时还能更快地生成IR
(中间表示) - 最后
AST
会生成IR
,IR
是一种更接近机器码的语言,区别在于和平台无关,通过IR
可以生成多份适合不同平台的机器码。对于iOS
系统,IR
生成的可执行文件就是Mach-O
加载动态链接库
查看一个 APP 都使用了哪些动态库,包括系统自带的动态库和第三方动态库,这些动态库将会在动态链接过程中被加载
找到App可执行文件路径,通过 otool
命令
1 | $ otool -L TestMain |
-L 参数打印出所有 link 的 framework(去掉了版本信息如下)
1 | TestMain: |
可以看到有两个默认添加的 lib
: libobjc
(objc
和 runtime
), libSystem
中包含了很多系统级别的 lib
, 比如我们熟知的 libdispatch
(GCD), libsystem_c
(C语言库), libsystem_blocks
(Block), libcommonCrypto
(加密库,比如常用的 md5
函数)
这些 lib
都是 dylib
格式的(如 Windows
中的 dll
), 使用动态链接有如下优点:
- 代码共用: 很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份
- 易于维护: 由于被依赖的 lib 是程序执行时才 link 的,所以这些 lib 很容易做更新,比如
libSystem.dylib
是libSystem.B.dylib
的替身,哪天想升级直接换成libSystem.C.dylib
然后再替换替身就行了 - 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多
dyld
dyld(the dynamic link editor)
, 动态链接器,概述 dyld
做了如下几件事:
- 先执行
Mach-O
文件,根据Mach-O
文件里undefined
的符号加载对应的动态库,系统会设置一个共享缓存来解决加载的递归依赖问题 - 加载后,将
undefined
的符号绑定到动态库里对应的地址上 - 最后再处理
+load
方法,main
函数返回后运行static terminator
ImageLoader
将文件加载进内存,且每一个文件对应一个 ImageLoader
实例来负责加载。
- 在程序运行时它先将动态链接的
image
递归加载 (也就是上面加载的递归依赖问题) - 再从可执行文件
image
递归加载所有符号
总结
dyld
开始将程序二进制文件(Mach-O
)初始化- 交由
ImageLoader
读取image
,其中包含了我们的类、方法等各种符号 - 由于
runtime
向dyld
绑定了回调,当image
加载到内存后,dyld
会通知runtime
进行处理 runtime
接手后调用map_images
做解析和处理,接下来load_images
中调用call_load_methods
方法,遍历所有加载进来的Class
,按继承层级依次调用Class
的+load
方法和其Category
的+load
方法
问题
Q: 重载自己 Class
的 +load
方法时需不需要调父类?
A: runtime
负责按继承顺序递归调用,所以不能调 super
Q: 在自己 Class
的 +load
方法时能不能替换系统 framework
(比如 UIKit
)中的某个类的方法实现
A: 可以,因为动态链接过程中,所有依赖库的类是先于自己的类加载的
Q: 重载 +load
时需要手动添加 @autoreleasepool
么?
A: 不需要,在 runtime
调用 +load
方法前后是加了 objc_autoreleasePoolPush()
和 objc_autoreleasePoolPop()
的。
Q: 想让一个类的 +load
方法被调用是否需要在某个地方 import
这个文件?
A: 不需要,只要这个类的符号被编译到最后的可执行文件中,+load
方法就会被调用(Reveal SDK 就是利用这一点,只要引入到工程中就能工作)