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 就是利用这一点,只要引入到工程中就能工作)
