直接阅读 Runtime 的代码并不合适,即使是使用 clang rewriter 直接重写 objc 到“原始代码” 也只是适合理解原理,最好是有一个能够跑起来的 objc-runtime 环境,可以做任意操作,修改,跟踪执行步骤,中断,log,调试等等。截止到目前 objc4 官方最新的版本是680,下载之后打开项目进行编译会出现缺少头文件的编译错误,需要补充缺少的头文件,参考这篇文章 build objc4 runtime,补充完整之后,新建 terminal target 打断点进行调试即可。不过 github 上已经有修改过的 objc4-680 源码,直接下载即可

程序启动流程

一个 iOS 程序会依赖于某些动态库,可以通过 otool -v -L debug-objc 命令查看,在启动的时候需要由动态链接器进行动态链接,而动态链接器自身也是动态库,因此动态链接器会首先自举自身,然后再将程序依赖的动态库依次加载进内存进行链接,动态链接做完之后,把控制权交给程序的 main 函数。通过 otool 命令可以查看到依赖列表中有 /usr/lib/libSystem.B.dylib 和 /usr/lib/libobjc.A.dylib 这样两条记录。libSystem.B.dylib 在编译的时候依赖于很多系统级别的 lib,如 libdispatch,libsystem_c,libcommonCrypto等等。而这个 libobjc.A.dylib 就是 objc-runtime,其初始化函数是 _objc_init,可以根据这个函数名称设置 symbol breakpoint 进行后续的调试,如下图:

1、首先系统内核建立进程,创建虚拟空间,读取 Mach-O 文件头,并建立虚拟空间与 Mach-O 文件的映射关系,然后找到 dyld 动态链接器入口 _dyld_start 之后,控制权交给动态链接器,接下来就是 dyld 的自举,即 dyldbootstrap::start() ,dyld 是开源的,相应的源码如下:

2、自举过程完成,即在 dyldbootstrap::start() 函数的最末尾会调用 dyld::_main 函数,并把此函数的返回值返还给 _dyld_start,这个返回值是 iOS 可执行程序 main 函数的地址,_dyld_start 会跳到该地址继续执行,进入程序的主逻辑

3、不过在这个地址返回之前,也就是在 dyld::_main 函数中需要做两个特别重要的工作:重定位和初始化,也就是动态链接的过程。在完成了自举之后,链接器需要装载动态链接库,然后完成符号的重定位;如果动态链接库文件中还有相应的初始化信息,即含有 __mod_init_func section,就再需要调用其相应的初始化函数

4、在 dyld::initializeMainExecutable 函数中,程序所依赖的动态库进行各自的初始化, 因此 ImageLoader::runInitializers 函数被调用,这个函数调用 ImageLoader::processInitializers 来处理初始化,而某个动态库可能依赖于其它的动态库,那么它所依赖的动态库就需要先初始化,这里形成一个递归,也就是 ImageLoader::recursiveInitialization

5、动态库初始化函数的真正调用是在 ImageLoaderMachO::doModInitFunctions 函数中,对于 libSystem.B.dylib 来说其初始化函数是 libSystem_initializer,在这个函数中 libdispatch_init 被调用,libSystem 以及 libdispatch 也是开源的,可以查看相关源码

6、在 libdispatch_init 中,对 Runtime 的 _objc_init 进行了调用,而在 Runtime 的初始化过程中,查看源码可以看到 Runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 Runtime 进行处理,Runtime 接手后调用 map_images 做解析和处理,把 Category 的实例方法、协议以及属性添加到类上,把 Category 的类方法和协议添加到类的 metaclass 上;接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 load 方法和其 Category 的 load 方法

7、在所有的动态库做好符号重定位和初始化工作之后,也就是 dyld::_main 临近末尾的时候,dyld 会获取 main 函数的地址返回给 dyld,dyld 紧接着调用 main 函数,将控制权交换给主程序,程序开始真正的执行,如下图: