起源 _objc_init
严格来说
_objc_init
并不是最开始的起点,_dyld_start
才是。这里暂不讨论dyld
的加载。
一切要从_objc_init
方法说起。_objc_init
是 Objc 的初始化入口,由系统调用。这里除了必要的初始化外,向dyld
注册了相关的回调,类(对象)的处理就是这些回调中进行的。
1 |
|
这个函数接收 3 个函数指针,分别用于接收加载镜像、初始化镜像、卸载镜像的事件。下面是这 3 个流程的大致说明。
接下来,我们就以这三条主线来研究系统是处理如何类(对象)的。
map_images
在map_images
阶段,又可以分为两个步骤:
- 准备数据:统计镜像、
SEL
、Message
,初始化全局数据结构 - 加载类数据:根据统计的数量加载类、分类、协议以及实现
non-lazy class
其中步骤 2 是核心。但在正式开始之前,我们需要先了解下几个相关内容:future classes
、remapped classes
、gdb_objc_realized_classes
和nonmeta classes
。这几个概念并没有详细的说明,但分析map_images
又不可避免。所以下面采用顺藤摸瓜的方式探索,一起瞅瞅吧。
future class
该类型的class
存储在NXMapTable
类型的future_named_class_map
全局变量中。根据注释,NXMapTable
是一种类似字典的结构,其中key-value
必须是指针或整形。下面是future_named_class_map
变量的调用路径图:
初始化及基础支撑
futureNamedClasses()
、haveFutureNamedClasses()
、popFutureNamedClass()
三个函数是直接访问future_named_class_map
的,分别负责:初始化、判断是否存在future class
、弹出一个future class
。
数据的添加
objc_getFutureClass()
调用_objc_allocateFutureClass()
是向future_named_class_map
中插入数据的唯一入口,但是objc_getFutureClass
却没有被调用。_objc_allocateFutureClass()
的实现也比较简单:
1 |
|
而addFutureNamedClass()
负责分配rw
、ro
内存并关联到类数据中。需要注意的是,此时的数据中除了rw
的flag
加上了future
的标识,其他均没有值。
1 |
|
数据的消费
future class
的唯一消费者是:readClass()
。该方法负责读取编译器写入的类,在读取过程中会根据类名读取future class
(如果有的话),并添加到remapped_class_map
或gdb_objc_realized_classes
或allocatedClasses
记录中。
1 |
|
需要注意的是readClass()
并不是每次都会调用,它需要在mustReadClasses()
返回true
的时候才会调用。而这一流程正是在后续的_read_images()
中。
至于readClass()
的其他两个调用者:_objc_realizeClassFromSwift()
和objc_readClassPair()
也没有调用者,无法看出其是如何使用的。
至此,future class
任然是个迷,只是知道系统会在某一个合适的时机生成、然后通过 readClass 读取其信息。
remapped class
该类型的class
存储在objc::LazyInitDenseMap<Class, Class>
类型的remapped_class_map
静态变量中。根据说明,这里主要存储两种情况下进行重映射的类:
- 现有类-已经实现的
future class
键值对 - 现有类-nil 键值对,该类属于
weak-linked
类型
下面参考调用路径,继续分析:
初始化
remappedClasses()
函数是负责分配存储该类型class
内存的,这是一个局部静态变量。
数据的添加
addRemappedClass()
负责向remapped_class_map
中插入一个{oldcls, newcls}
键值对。
数据的消费
既然存在这样的重映射,那么对于给定一个class
,可能不是最终使用的class
,所以就需要一个方法去确定这个情况。remapClass()
就是干这个的:
1 |
|
按照上面的线索,可以知道remapped_class_map
存储了那些原始类和使用类不统一的类。
gdb_objc_realized_classes & nonmeta_class_map
gdb_objc_realized_classes
表存储了不在dyld
共享缓存中的类,该类可能已经被实现或者没有。
nonmeta_class_map
表是gdb_objc_realized_classes
表的补充,只存储了一种类型的类:在主表gdb_objc_realized_classes
存在,但不是因为同名
因素而存在的。
这里不是很好理解,原注释是: It only contains metaclasses whose classes would be in the runtime-allocated named-class table, but are not because some other class with the same name is in that table.
这两张表也都是NXMapTable
类型。不同的是,前者存储name-class
键值对,后者存储了metaclass-class
键值对。
下面参考其对应的调用图继续分析:
初始化
gdb_objc_realized_classes
的初始化时在_read_images()
函数中进行的。初始化时分配的内存为之前统计到类数量的4/3
倍。而nonmeta_class_map
的初始化是通过懒加载的方式进行,由其getter
方法nonMetaClasses()
负责。
数据的添加
addNamedClass()
负责向gdb_objc_realized_classes
插入一个name-class
键值对,或向nonmeta_class_map
中插入metaclass-class
键值对:
1 |
|
可以看到,在使用名称从getClassExceptSomeSwift()
函数查询到class
,并且和入参replacing
不相等的时候,会插入到nonmeta_class_map
中。再看看getClassExceptSomeSwift()
函数的实现:
1 |
|
这里的实现也比较简单,就是通过名称,调用getClass_impl()
函数进行查找;若是Swift
类名,还会多一次查找。所以getClass_impl()
才是核心:
1 |
|
通过上面的分析,我们知道在addNamedClass()
中,一个类具体会插入到哪个表中有几种情况:
getClassExceptSomeSwift()
未查找到 - 记录插入主表getClassExceptSomeSwift()
查找到, 结果和入参replacing
不相等 - 记录插入的附表getClassExceptSomeSwift()
查找到, 结果和入参replacing
相等 - 记录插入主表 在整个类初始化过程中,使用getClassExceptSomeSwift()
函数查找类时,是查不到的,所以程序中多数的类会被插入到gdb_objc_realized_classes
表中。
最后,对应addNamedClass()
的入参replacing
,我排查了对应的几个调用方,它只有 2 种可能:
nil
默认值,在_objc_realizeClassFromSwift()
和objc_duplicateClass()
以及objc_registerClassPair
过程中也传入的是nil
future class
在readClass()
过程中
所以,在replacing
有值时,它一定是future class
。
数据的消费
类对象被添加到主表和附表中后,会在程序后续的运行中进行访问,具体细节这里先不分析。最后,在程序镜像卸载时,通过removeNamedClass()
进行移除:
1 |
|
综上,我们知道了,在程序中使用的类对象大多添加到了gdb_objc_realized_classes
,剩下一部分会添加到nonmeta_class_map
中。
_read_images
_read_images
函数主要是读取镜像中的信息,包括@selector
引用、类、分类、协议。依据前面讲到的内容,这里主要分析类相关的内容。
Discover classes
这里主要扫描镜像中的类列表,并通过readClass()
函数将类添加到记录表中,并返回。返回值可能是nil
、类本身
或future class
。若返回了future class
,这里还会记录,后续将这种类型的类通过realizeClassWithoutSwift()
函数实现。
Fix up remapped classes
在Discover classes阶段,调用readClass()
可能会读取到需要进行重映射的类。这个阶段会将镜像中的类引用,修复到映射之后的值上。
1 |
|
Discover categories
这里主要扫描镜像中的分类列表,然后根据类是否Realized
采取不同的动作。
- 对于
Realized
的类,将分类中的实例方法、实例属性添加到类对象上;将类方法、类属性添加到元类对象上(OC 居然可以添加类属性,表示没用过)。这个过程主要的功臣是attachCategories()
函数。 - 对于暂未
Realized
的类,只是简单的添加到unattachedCategories
,后续通过methodizeClass
处理。
attachCategories()
:
1 |
|
需要注意的是,attachCategories
方法是调用attachLists
结构中的attachLists
方法完成插入的。这个方法有个特点,在之前的文章中有提到,它会将之前的数据后移,将新的数据放置在前面。所以会导致最后加载的分类方法在列表最前面,存在同名方法按续查找会覆盖。
methodizeClass
:
1 |
|
Realize non-lazy class
non-lazy
类型的类是指实现了load
方法或者存在对应的静态变量。该阶段会扫描non-lazy
类并将其实现,然后记录到allocatedClasses
表中。这里的实现主要做了几件事情:
- 分配
RW
内存 - 分配
class
索引 - 实现当前类的父类及元类
- 确定是否使用原始
isa
指针 - 初始化
isa
- 修复
ivar
偏移量 - 关联类与父类关系
- 关联分类
具体参考下面的代码:
1 |
|
load_images
该阶段主要处理类的 load 方法。思路比较清晰,先准备,然后调用。
prepare_load_methods
准备 load 方法时,先处理类,再处理分类。在处理类的时候,会优先考虑类的父类。具体见代码:
1 |
|
call_load_methods
经过上一步的 prepare,会得到loadable_classes
和loadable_categories
两个数组,分别存储类和分类的 load 方法信息。下面就是顺次遍历调用了:
1 |
|
需要注意的是,在准备和调用时均是直接操作函数地址,并没有走 OC 的消息机制。
unmap_image
这里负责卸载不在使用的镜像。是主要包含:
- 卸载镜像
- 移除 Header 信息
其中卸载镜像时,会先移除分类,再移除类及其元类并释放相关内存。具体细节参考unmap_image_nolock
和_unload_image
unmap_image_nolock
:
1 |
|
_unload_image
:
1 |
|
总结
好了,到这里可以暂时舒缓下了,一起梳理下该篇讲的内容。我们以镜像的使用周期为主线,分别探索了镜像的读取(包含类、分类以及类的实现等),镜像的加载(load 方法的扫描及调用),以及镜像的卸载。
这里还遗留了部分问题没有解决,比如:
methodizeClass
主要处理了ro
中的方法以及分类中的信息,其中涉及到的方法排序具体是怎么样的?non-lazy class
是在_read_images
中进行了实现,那其他的类是在什么时候实现的?- …
希望大家带着自己的疑问,继续前行。共勉!