Objective-c中的runtime学习记录

作为iOS开发者,在使用objective-c进行编程的时候,最熟悉的就是各种类,对象,消息,但是实际上类和对象的本质是什么呢?消息在运行时如何实现?这些问题如果不稍稍深入探究一下可以说永远只是浮于表面,一个合格的程序员我想应该是有动力去探究这些内核机制的实现的。
所以这里就来说说关于OC对象,消息和运行期的事情(参考Effective Objective-c 2.0)

类对象

首先要说的应该是稍微了解一点objective-c的程序员都了解的事情:在objective-c中,不管是对象还是类,其本质都是C的结构体。所有的类和对象结构体都包含一个 isa_t 联合体的指针(早前的文档中称为isa指针,但是现在已经变为专门的联合体isa_t了),可以看一下这个 isa_t 联合体的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
// extra_rc must be the MSB-most field (so it matches carry/overflow flags)
// nonpointer must be the LSB (fixme or get rid of it)
// shiftcls must occupy the same bits that a real class pointer would
// bits + RC_ONE is equivalent to extra_rc + 1
// RC_HALF is the high bit of extra_rc (i.e. half of its range)
// future expansion:
// uintptr_t fast_rr : 1; // no r/r overrides
// uintptr_t lock : 2; // lock for atomic property, @synch
// uintptr_t extraBytes : 1; // allocated with extra bytes
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};

乍看起来并不好懂,前面的部分比较简单,结构体本身的默认构造函数,以及指向目标类的Class 指针(这个Class 本身就是 objc_class结构体类型的指针,后面再说)。毕竟 isa 指针就是用来指向一个对象或者类所属的类的指针,这没有什么疑问。
后面的分为arm64 架构和 x86_84 架构上的实现,这取决于不同的机器的指令集。但是在这里我们只要知道这些该有的字段全部都有,只是长度不同而已就行了,两种架构上的机制其实大同小异。所以我们那x86_64架构来分析就可以了。

1
2
3
4
5
6
7
8
9
10
11
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};

这是isa_t中的isa结构体,一共占据64位的内存空间,可以看到不同的字段有不同的长度,它们也对应不同的数据含义。
首先我们看一下当一个对象初始化的时候,调用了一个函数(简化过后):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}

这里为什么会出现两个判断呢?如果isa不是nonpointer,也就是单纯的指针(Class)类型,那么久直接将isa赋值为cls。否则就是isa_t类型,应该先初始化一个所有位为0的指针isa_t。基本上这也是不同版本的处理方式。查看SUPPORT_INDEX_ISA的宏,发现是ARM_ARCH_7K > 2这样的判断,不太明白具体是指什么方面的架构=。=可能是对Watch的优化吧。
不过我们基本上只关心else之后的内容。
可以看到,先将 newisa的bits赋值为常量ISA_MAGIC_VALUE,里面包含的是magic 和 nonpointer 的值。然后标记“是否有c++析构函数”,最后将移位后的 cls 存入 shiftcls。

  • 上面说的 nonpointer 字段用来表示isa_t的类型,如果是0,表示这个isa_t中不包含结构体,访问对象的isa会直接返回一个 cls 的指针。而 nonpointer如果是1,表示 isa 不是指针,cls的信息只是作为一部分,这些类的指针的信息就保存在shiftcls中。(也就是说两个对象的isa 有可能根本结构上就不一样)
    而 magic 用来判断当前对象是真的对象还是没有初始化的空间。
  • 而has_cxx_dtor 这个字段表示当前对象是否有C++或objc的析构器,如果没有就会快速释放内存。
  • 内容最多的 shiftcls,前面我们说了,类本身也是一个结构体,一个对象的isa指针最重要的功能就是能找到其类对象。这里调用
    = (unintptr_t)cls >> 3;``` 将当前地址右移三位是为了字节对齐,objc中的类指针的地址后三位一定是0,所以这里省去后三位能节省内存消耗。在MAC的系统上,使用47位作为指针(IPhone上只用了33位),所以减去三位,我们就明白shiftcls为什么占用44位的空间了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    * 通常我们不会直接查找 isa 结构体,而是使用系统提供的ISA()方法来间接获取。
    * 其他的字段还有:``` has_assoc```对象含有或者曾经含有关联引用,如果没有就能更快地释放内存;``` weakly_referenced``` 对象呗指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放内存;``` deallocating``` 对象正在释放内存;``` has_sidetable_rc```对象的引用计数超过最大容量;``` extra_rc```当对象的引用计数超过1的时候会将多的部分保存在这个字段里,比如5就保存4,如果超过最大就告诉has_sidetable_rc超过了。
    说完了isa,再具体回到类对象这个概念,先看看class结构体的样子:
    ```objective-c
    struct objc_class : objc_object {
    // Class ISA;
    isa_t isa; // 在objc_object中定义
    Class superclass;
    cache_t cache; // formerly cache pointer and vtable
    class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

所以这里很明白的看出来,objc中的类也是一个对象,所以我们才说有类对象这个概念,不过不像普通的对象能生成很多次,这里的类对象的初始化全部由系统来控制,也只会创建一个而已。
关于objc中的类,有一点很重要的事情:在objc中,对象的方法并没有存储在对象的结构体中,当实例方法被调用的时候,它必须通过自己持有的 isa 来查找对应的类,然后再这里的 class_data_bits_t 结构体中查找对应方法的实现。并且每个objc_class 也有指向父类的指针 super_class来查找继承的方法,(这一条消息传递转发是objc的基础,展开讲很复杂,后面详细说明)。
可以想想到查找到最后一定要有一个终点,objc引入了 元类 这个概念来保证无论是类还是对象都能通过相同的机制查找方法。我们知道对象的 isa 指向的是其类,而类的 isa 呢?类的 isa 指向的是其“元类 metaclass”(千万不要把元类和父类弄混)。实例方法查找对象的 isa , 而类方法查找类的 isa。
有一张很经典的图片能够完美概括对象,类和元类的关系:

metaclass

理解消息机制

上面我们对objc中所谓的类和对象的实现有了基本的认识,接下来看objc中非常重要的机制————消息。
在对象上调用方法是objc中经常使用的功能,用objc的术语来说就是“消息传递”,消息有名称(name)和选择子(selector),可以接受参数,而且可能还有返回值。这个消息传递有什么特别之处呢?我们得先来说说objc的超集C:C语言使用静态绑定(static binding),也就是说在编译期就能决定运行时所调用的函数,比如我们有两个函数printHello和printWorld,在编译代码的时候就已经知道程序中包含这两个函数了,然后直接生成调用这个两个函数的指令,而函数地址也是硬编码到指令中的。

而在objc中,如果向某对象传递信息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象只有在运行的时候收到消息之后才能决定该调用哪个方法。比如我们有下列一个方法调用:

1
id returnValue = [someObject messageName:parameter];

编译器看到这个消息之后将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其函数原型如下:

1
void objc_msgSend(id self, SEL cmd, ...)

这个函数的参数是可变的,第一个参数表示消息的接受者,放到这里就是someObject,第二个参数是选择子selector,在这里就是messageName,后续的参数就是消息中的各种参数,按照顺序依次加入。那么实际上我们在objc中调用的方法,被编译器转换后的样子是这样的:

1
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend函数会根据接收者与选择子的类型来调用适当的方法。为了完成此操作,更改方法需要再接收者所属的类中搜寻其“方法列表(list of methods)”,如果能够找到对应的方法,就直接跳转至实现代码。如果找不到,就沿着继承机制向上查找,等找到合适的再跳转,如果最终还是找不到,就只能执行消息转发。
所幸的是objc_msgSend调用了一个方法之后会将匹配结果缓存在快速映射表(fast map)里,每个类都有一个这样的缓存,这样后续再执行同样的方法的时候速度就会快很多。

当然objc_msgSend还有一些其他的形态用来处理一些特殊情况:(1) objc_msgSend_stret,如果待发送的消息要返回结构体,就交由这个函数来处理,只有在CPU的寄存器能够容纳的下消息返回类型的时候,这个函数才能处理此消息 (2) objc_msgSend_fpret, 如果消息返回的是浮点数,那么交给这个函数来处理,因为某些架构的CPU中调用函数时,需要对“浮点数寄存器”做特殊处理,所以必须调用特殊的函数 (3) objc_msgSendSuper, 这个看名字就知道,给超类发送消息的时候调用。

其实我们说的选择子也并不是真正调用的函数本身,实际上编译器会把每个对象的方法全部编译为C函数,然后将函数指针保存在每个类都有的表格中。选择子只是一个从表格中查询到具体函数的键而已。

下面接着说消息转发机制,上面我们已经知道了,消息会通过objc_msgSend函数通知给接收者执行具体方法,但是很多时候并不是那么顺利就能在类的方法列表或者其父类中找到对应的方法的。objc有一套对应的机制来处理消息的转发,尽量使消息能够被合理地处理,毕竟如果没有找到处理消息的办法就会导致程序崩溃,所以必须尽量避免这种情况发生。

消息转发分为两大阶段,第一阶段先征询接收者所属的类,看其是否能动态添加方法,以处理当前未知的selector,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。我们当然希望能在第一阶段就把问题解决掉,但是总还是会遇到进入第二阶段的情况,这又细分为两小步。首先看看其他对象有没有能处理这个消息的,可以就转发给它,如果没有则启动完整的消息转发机制,将消息有关的细节封装到NSInvocation中,最后一次让接收者解决当前的问题。

动态方法解析:对象在收到无法解读的消息之后,首先将调用其所属的下列类方法:

1
+ (BOOL)resolveInstanceMethod:(SEL)selector

如果是类方法就调用resolveClassMethod,本质是差不多的,都是本类为了解决无法识别的选择子的问题而提前准备的方法,比如CoreData中实现dynamic属性,因为存取方法在不用等待运行期才能确定,所以可以提前写在上面所说的方法的实现中,当进入动态方法解析的部分时,直接调用实现就行了。不过这种方法,只在当我们预先知道怎么处理特定选择子的情况下才有用。

备用接收者:如果动态方法解析不行,接收者还有第二次机会能处理未知的选择子,系统会让它寻找其他能处理的接收者,需要实现的方法如下:

1
- (id)forwardingTargetForSelector:(SEL)selector;

同样也是像上面一样,得预先准备好哪些类能处理对应的selector,如果最终返回nil表示找不到能够执行的其他类(这个机制能够用来模拟出多重继承的一些特性,一个对象可以将方法转发给几个不同的对象来执行,从外界看来和多重继承很像)。但是实际上很多时候我们也没办法提前知道什么类能够处理转发问题。

完整消息转发:如果上面两个方案都没法解决问题,那么只能调用完整的消息转发机制了。先创建NSInvocation对象,把信息相关的全部细节封装其中。然后系统调用

1
- (void)forwardInvocation:(NSInvocation *)invocation;

这个方法只需要改变调用目标就行了,但是仅仅这样改变和前面的备用接收者没什么差别,所以通常会连消息的内容也会稍作改变,比如追加参数或者改变选择子等等。
最终我们可以看到消息的转发机制如下图所示

这些操作,越靠后的代价越大,所以尽量能在前面解决就在前面解决。(但是我怎么觉得多的反而是靠这些机制实现其他的特性=。=)

消息转发流程

属性和方法

最开始我们介绍了isa和类对象的概念,回想起来还有具体的结构没有细说,先来回忆一下objc_class的结构:

1
2
3
4
5
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

其中bits就是存储类的方法、属性、遵循的协议等信息的地方。

1
2
3
4
5
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
}

class_data_bits_t结构体只含有一个64位的bits用来存储和类有关的信息,而objc_class中的注释写到class_data_bits_t相当于class_rw_t加上rr/alloc的flag。也就是说bits为我们提供了便携方法能够返回其中的 class_rw_t* 指针

1
2
3
4
5
6
7
8
9
// objc_class
class_rw_t *data() {
return bits.data();
}
// class_data_bits_t
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

将bits 和 FAST_DATA_MASK 作位运算,只取其中的[3,47]位转换成 class_rw_t* 返回。objc类中的属性、方法还有遵循的协议都保存在 class_rw_t 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
};

其中class_ro_t结构体又存储了当前类在编译时就确定的属性,方法和遵循的协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};

这个流程是这样的:在编译期间 clss_ro_t 就已经确定了,objc_class中的bits的data中保存着具体数据,然后等runtime开始加载,程序真正执行的时候调用 realizeClass 方法,将 class_ro_t 中的数据转换到 class_rw_t 结构体中,然后重新设置结构体 ro 和 flag。不过这之后 class_rw_t 中的方法、属性以及协议列表还是为空,需要调用 methodizeClass 方法来把空缺的内容填满。 简单来说就是 class_ro_t 是编译期记录的原始数据,而 class_rw_t 是通过运行期通过前者得到的真实被访问的数据

存放实例变量的结构体 ivar_t 的具体构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};

其中name表示实例变量的名字,type表示实例变量的类型,size表示实例变量的大小,而offset为实例变量距离对象的首地址的偏移量。
在 class_ro_t 结构体中有两个非常重要的变量 instanceStart 和 instanceSize,前者是对象开始存放实例变量的地址的偏移量,后者是对象的大小(isa_t + ivarInt + propertyInt),实际操作中取出实例变量都是靠对象的偏移量计算来得到的。

再来看看保存方法的结构体:

1
2
3
4
5
struct method_t {
SEL name;
const char *types;
IMP imp;
};

变量的名称很通俗易懂,就不展开讨论了。总结来说在runtime运行刚开始保存一个类的信息的都是 class_ro_t,然后调用 realizeClass 方法才将其转换成 class_rw_t 结构体。但是为什么要这样操作呢?
类在内存中的位置是编译期间决定的,在之后修改代码,也不能改变内存的位置,但是由于objc是动态语言,在运行时会添加各种新的属性,方法和协议,所以不能一开始就将这些保存信息的数据放到和类在内存中相同的地方。必须在别的地方创建了能在运行时添加方法的 class_rw_t 结构后再将其和类放到一起。所以说是先有了 class_ro_t 之后才能创建 class_rw_t。

分类category关联对象

最后来看看,runtime如何给已经存在的类动态添加特征,这是objc中很突出的特点。
一般当我们想为某个类添加方法的时候会使用category,从这里可以看出它和extension的不同:category不能添加实例变量,这表明它并不是类的一部分,因为在运行期添加实例变量会打破类的布局,造成灾难性的结果。
我们看看category的结构:

1
2
3
4
5
6
7
8
typedef struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
} category_t;

包含了类的名字,类本身,category要添加的实例方法的列表,category要添加的类方法的列表,添加的协议的列表,还有添加的所有属性。
objc开始运行时的入口方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
}

而最后一个函数的调用中就将category加入到了类上,再看objc_runtime_new.mm文件中_read_images方法,最后有这么一个片段;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 给class本身添加各种列表
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
// 给class的isa也添加各种列表
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}

这个拿到的catlist就是编译器准备的category_t结构体的数组(一个类可能有不止一个categoty),上面的代码实际上完成的工作就是(1)将categoty的实例方法、属性和协议添加到类上 (2)将category的类方法和协议添加到类的metaclass上。上面的代码中,addUntachedCategoryForClass这个函数只是把类和category做了一个关联映射,而remethodizeClass才是真正将category添加到类上面的核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
而这里又交给另一个函数来处理:
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}

这个方法中主要是将category的实例方法列表、属性列表和协议列表分别拼成三个大的列表,j更深入的方法教给attachLists函数来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}

这里有个关键的部分就是categoty中的元素并没有替换掉原来已经存在的类的方法,而是在原基础上添加。考虑到实际查找方法的顺序,其实这样category添加的方法,如果和类的原方法重名的话,是category方法会被调用。这样看上去就像是category中的方法“覆盖”掉了原方法一样,但是要注意的是实际上机制并不是单纯的覆盖。

既然说到了category,就不得不提到能给类添加变量的方法—-关联对象(Associated Object)了。使用关联对象的函数原型如下:

1
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

一路查找下去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}

这其中涉及到几个类,分别有不同的作用:(1)AssociationsManager–它管理一个自旋锁到哈希表的单例映射,通过associations()方法可以去到管理的AssociationHashMap单例 (2)AssociationsHashMap–一个无序哈希表,存储的是对象的地址到ObjectAssociationMap的映射 (3)ObjectAssiciationMap–存储了key到被关联对象ObjcAssociation的映射 (4)ObjcAssociation–存储关联对象的信息。
简单解释上面函数的流程就是先获取AssociationsManager,然后通过associations()方法获得AssociationsHashMap单例(全部的关联变量都由这一个单例负责维护),然后通过需要添加变量的对象object的地址来取得它对应的一个子哈希表(AssociationsHashMap中的一条,里面全部是这个对象的关联对象),进而看有没有已经存在的关联变量的记录,如果有就覆盖,如果没有就新添加一个关联变量。

知道了set的流程,其实get和remove也显而易见了,只要知道关键在于两层哈希表就行了。同样我们也能理解到,其实关联对象并不是直接在内存上和本对象那么紧密,只是将关联对象放在了一个全局的哈希表中方便维护查找而已,结果上看也不是真正的添加了一个实例变量。

总结

以上粗浅地谈了一下关于runtime源码中自己了解到的类和对象以及属性方法的底层实现机制。参考了很多别人的博客和文档,还有开源的runtime的方法。实际上也只是皮毛而已,还需要更深入的了解和探索。

参考:

1.Draveness大神的博客: 从NSObject的初始化了解isa

2.Effective Objective-c 2.0

3.runtime 源码

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器