众所周知,一个app的入口就是main.m 里面的main函数,接下来我们来剖根究底的探讨下调用main函数之前,程序都做了哪些事情?
动态链接库
iOS 中用到的所有系统 framework 都是动态链接的,类比成插头和插排,静态链接的代码在编译后的静态链接过程就将插头和插排一个个插好,运行时直接执行二进制文件;而动态链接需要在程序启动时去完成“插插销”的过程,所以在我们写的代码执行前,动态连接器需要完成准备工作。
使用otool 可以查看隐藏的动态链接库
系统使用动态链接有几点好处:
- 代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份
- 易于维护:由于被依赖的 lib 是程序执行时才 link 的,所以这些 lib 很容易做更新,比如
libSystem.dylib
是libSystem.B.dylib
的替身,哪天想升级直接换成libSystem.C.dylib
然后再替换替身就行了 - 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多
dyld 动态链接器
系统核心做好启动程序的准备工作后,交给dylb负责,对dylb的作用顺序概括如下:
- 从 kernel 留下的原始调用栈引导和启动自己
- 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
- 使用imageLoader将二进制文件(可执行文件或so文件),里面是编译过的符号代码等加载进内存,且每一个文件对应一个imageLoader实例来负责加载。dyld 会通知 runtime 进行处理,runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法
- non-lazy 符号立即 link 到可执行文件,lazy 的存表里
- Runs static initializers for the executable
- 找到可执行文件的 main 函数,准备参数并调用
- 程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
- 程序main函数 return 后执行 static terminator
- 某些场景下 main 函数结束后调 libSystem 的 _exit 函数
总结
整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,
动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。
值得说明的是,这个过程远比写出来的要复杂,这里只提到了 runtime 这个分支,还有像 GCD
、XPC
等重头的系统库初始化分支没有提及(当然,有缓存机制在,它们也不会玩命初始化),总结起来就是 main 函数执行之前,系统做了茫茫多的加载和初始化工作,但都被很好的隐藏了,我们无需关心。