「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。
在之前几篇的几篇博客中,已经完成了对类的数据结构、方法查找的分析,本篇将开始对类的加载过程进行一些分析、记录。这个分析顺序的思路,举个大概的栗子就好像买了个毛坯房首先先了解了它的结构、布局如何,然后在将软硬装、家具等放进这个房子里。(本篇总字约2038字)
本篇重点:
_objc_init方法对于runtime环境的初始化、ReadImage、LoadImage的方法注册。- Readimage 的加载过程、各种记录表的创建、对Cls的实现、Cls中的数据加载。
- Category 的附加过程、附加时机、附加算法。
一、runtime初始化
从静态代码 --> dyld链接image --> 加载进内存,代码是如何被装载到内存变成可以被调用、被执行的方法的?被创建的类中的ro、rw、rwe又是从哪里来的呢?带着疑问先从 _objc_init 着手开始分析:
首先从图左侧的调用堆栈可以看出,_objc_init 正好是从 _dyld_start汇编到C++源码的分界点,也是runtime的初始化节点。接下来先对_objc_init中的各个方法的作用做个总的说明,然后再分别展开分析:
environ_init:初始化环境变量。tls_init: **初始化 线程局部存储。**线程局部存储是操作系统为线程单独提供的私有空间,具有很有限的空间。static_init: 运行C++静态构造函数。runtime_init:初始化unattachedCategories、allocatedClasses 表。exception_init:初始化异常处理环境。cache_t::init:初始化缓存。_imp_implementationWithBlock_init:启动回调机制。_dyld_objc_notify_register: 向dyld的注册回调。
1.1、environ_init
这个方法的主要作用是读取影响运行时的环境变量,如OBJC_DISABLE_NONPOINTER_ISA (只用RwaIsa) 、OBJC_PRINT_LOAD_METHODS (打印所有+Load) 等等,先进入方法内看一下代码:
- 如图所示,整个方法最核心的代码就是红框标注的循环:读取并设置环境变量的值。
- 最末尾For循环的则是根据
OBJC_HELP、PrintOptions等判断条件将变量设置的结果进行打印。 - 分析循环中代码可以看出来,无论上下哪个循环都是在对
Settings[]进行操作,那接着来看一下Settings又是什么结构?Settings是一个结构体数组,数组内包含objc-env.h中定义的所有环境变量。- 除了在
objc-env.h中查看这些变量,还可以在Terminal中输入export OBJC_HELP=1来输出、命令查看。
1、使用 环境变量
分析完environ_init 的代码逻辑,现在来记录一下如何使用这些变量。日常开发可以通过Xcode使用这些环境环境变量来帮助调试,可通过以下路径来编辑项目的 scheme,从而设置调试所需要的环境变量:
菜单Product --> Edit scheme --> 左侧Run栏目 --> 顶部Arguments --> Environment Variables
-
比如设置了
OBJC_PRINT_LOAD_METHODS环境变量,就可以将代码中实现过+[xxx load]的类统统输出到控制台中。
1.2、static_init
这个方法的主要作用如其注释所描述:运行C++静态构造函数。但这里的C++构造函数指的并不是项目中所有的构造函数,而只是包含定义在libobjc.A.dyld 中的。至于其他的C++构造函数要等到dyld 中的ImageLoaderMachO::doModInitFunctions 方法调用是被执行。
注意一点:
为什么要在这么早的时机自己调用C++方法呢,而不等着dyld?
因为程序运行到_objc_init时,已经在初始化整个runtime运行环境了。其中有许多的C++构造函数比如defineLockOrder()、_GLOBAL__sub_I_objc_class.mm、_GLOBAL__sub_I_objc_errors.mm等等,都是runtime环境运行所必需使用的,所以这些构造函数都要提被执行。
现在来看一下static_init 的代码实现:
-
如图标注,根据
getLibobjcInitializers、getLibobjcInitializerOffsets的内部实现可以确定读取的Section -
对于
getLibobjcInitializers的读取区域可以在_objc_init上方自定义一个C++方法,通过动态调试来比较取出来的count值的变化来验证。或者进一步将断点设置在自定仪的C++方法内,查看断点是否在循环的最后被执行。 -
不过
getLibobjcInitializerOffsets在分析过程中读出的count总是0,offsets也总是NULL 并且在dylib中也确实没找到对应的Section。
1.3、runtime_init
这个方法的主要作用就是对于两张表的初始化。先来看一下方法的代码实现,再分析这2张表分别要存储什么信息:
- unattachedCategories:Class、继承自
ExplicitInitDenseMap类,其中有扩展方法addForClass、attachToClass、eraseClass等。这张表用于存放的是还没附加到Cls的Categorys。(之后分析到Category的附加时,再展开分析) - allocatedClasses:
ExplicitInitDenseSet的类对象,注释中对这个标的描述是:所有已经被objc_allocateClassPair 方法Alloc的类(包括元类)。这个表再次见到不陌生,在前面的博客[[3.2 lookUpImpOrForward分析 id=b6562723-b86d-4fee-953c-95901ff7ae19]] 时,慢速查找流程的准备、检查过程中在checkIsKnownClass中就是向这个表中查找Cls,用于验证Cls的合法与安全。
1.4、exception_init
这个方法的作用如其注释描述:初始化libobjc的异常处理系统。看一下代码实现:
-
首选,想要更好的理解
exception_init的作用方法,就要先解释一下std::set_terminate库函数:std::set_terminate:可以设置自己的terminate函数。自定义的terminate函数不能有参数,且返回值类型为void。另外,terminate函数不能返回也不能抛出异常,它必须终止程序,如果terminate函数被调用,这就意味着问题已经无法解决了。
-
自定义的
_objc_terminate中,如果是objc的异常,会通过(*uncaught_handler)((id)e)向上层抛出,uncaught_handler同样是个句柄,在上层的代码中设置了对应的接收方法,就可以拦截到异常的信息。
1.5、*_dyld_objc_notify_register
_dyld_objc_notify_register方法仅供Objc-Runtime调用,主要作用就是向dyld中注册回调,以便在注册镜像、加载镜像、卸载镜像时,dyld能够回调mapped。所以,_dyld_objc_notify_register 是将image加载到内存的重要枢纽,也是对于类的加载过分析的关键。_dyld_objc_notify_register方法的代码实现在dyld源码中,所以这里一笔带过,可能以后会另起篇幅进行分析。
首先来看看方法的三个入参:(&map_images, load_images, unmap_image) ,能够明显的看到第一个参数map_images 是使用& 进行了地址传递,这意味着在dyld中和runtime中map_images 的操作是同步的,那么就先从map_images -映射镜像展开分析。
1.6、小结
以上就是对于_objc_init中对于runtime环境各个初始化方法的分析,最后从对_dyld_objc_notify_register方法的分析中引出了 map_images, 从map_images开始系统又在处理什么呢?下一篇继续。
本篇中分析、记录的内容如对你有帮助,欢迎点赞👍、收藏✨、评论✍️。如果错误,欢迎在评论中指正🙆🏻♂️