一个 iOS App 的 main 函数位于 main.m 中,这是我们熟知的程序入口。但对 objc 了解更多之后发现,程序在进入我们的 main 函数前已经执行了很多代码,比如熟知的动态库的加载, runtime 的初始化, + load 方法等。本文将跟随程序执行顺序,刨根问底,从 dyld 到 runtime ,看看程序从启动到 main 函数执行之前都发生了什么。
main之前的加载过程
- Mach-O 加载
- 从 dyld 开始
- 加载程序相关依赖库, 并对这些库进行链接;
- 调用每个依赖库的初始化方法,在这一步,runtime被初始化;
- 将程序依赖的动态链接库递归加载进内存;
- runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法;
- dyld返回main函数地址,main函数被调用;
- main函数
Mach-O 加载
关于 Mach-O 文件这里 有一篇 文章介绍 Mach-O 文件.
Mach-O文件格式是 OS X 与 iOS 系统上的可执行文件格式,像我们编译过程产生的 .O 文件,以及程序的可执行文件,动态库等都是Mach-O文件。它的结构如下:
有如下几个部分组成:
Header:保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等等。
LoadCommands:可以理解为加载命令,在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main函数的加载地址,程序所需的dyld的文件路径,以及相关依赖库的文件路径。
Data: 这里包含了具体的代码、数据等等。
系统加载程序可执行文件后,通过分析文件来获得dyld所在路径来加载dyld,然后就将后面的事情甩给 dyld 了。
从 dyld 开始
-
dyld: (the dynamic link editor)动态链接器,其源码是开源的。
dyld::_main函数源码
一切源于dyldStartup.s
这个文件,其中用汇编实现了名为__dyld_start
的方法,汇编太生涩,它主要干了两件事:
调用 dyldbootstrap::start()
方法(省去参数)
上个方法返回了main
函数地址,填入参数并调用main
函数
这个步骤随手就能验证出来,设置一个符号断点断在_objc_init:
这个函数是runtime的初始化函数,后面会提到。程序运行在很早的时候断住,这时候看调用栈:
看到了栈底的
dyldbootstrap::start()
方法,继而调用了 dyld::_main()
方法,其中完成了刚才说的递归加载动态库过程,由于 libSystem
默认引入,栈中出现了 libSystem_initializer
的初始化方法。
- ImageLoader: 用于辅助加载特定可执行文件格式的类,程序中对应实例可简称为image(如程序可执行文件,Framework库,bundle文件)。
两步走:
- 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻);
- 再从可执行文件 image 递归加载所有符号;
当然所有这些都发生在我们真正的 main 函数执行前。
-
runtime 与 +load
刚才说到libSystem
是若干个系统 lib 的集合,所以它只是一个容器 lib 而已,而且它也是开源的,里面实质上就一个文件,init.c,由libSystem_initializer
逐步调用到了_objc_init
,这里就是 objc 和 runtime 的初始化入口。
除了 runtime 环境的初始化外,_objc_init
中绑定了新 image 被加载后的 callback:
dyld_register_image_state_change_handler(
dyld_image_state_bound, 1, &map_images);
dyld_register_image_state_change_handler(
dyld_image_state_dependents_initialized, 0, &load_images);
可见 dyld 担当了 runtime 和 ImageLoader 中间的协调者,当新 image 加载进来后交由 runtime 去解析这个二进制文件的符号表和代码。继续上面的断点法,断住神秘的 +load 函数:清楚的看到整个调用栈和顺序:
dyld
开始将程序二进制文件初始化
交由ImageLoader
读取image
,其中包含了我们的类、方法等各种符号
由于runtime
向dyld
绑定了回调,当 image
加载到内存后,dyld
会通知 runtime
进行处理
runtime
接手后调用map_images
做解析和处理,接下来 load_images
中调用call_load_methods
方法,遍历所有加载进来的Class
,按继承层级依次调用Class
的+load
方法和其Category
的 +load
方法
至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)
都已经按格式成功加载到内存中,被runtime
所管理,再这之后,runtime
的那些方法(动态添加Class、swizzle
等等才能生效)
main函数
当所有的依赖库库的 lnitializer 都调用完后,dyld::main
函数会返回程序的 main 函数地址,main 函数被调用,从而代码来到了我们熟悉的程序入口。
结语
- 整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,
- 动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。
这里只是简单了概括了从程序启动->dyld加载依赖库->runtime初始化->main 的过程。