初探Category实现原理

Objective-C 分类的实现原理,阅读runtime源码会发现,分类在运行时的结构是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; // 属性列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta) {
if (isMeta) return nil; // classProperties;
else return instanceProperties;
}
};

name代表的是分类的名称;cls是分类所属的宿主类;classMethods是实例方法列表;classMethods类方法列表;protocols是协议列表;instanceProperties是属性列表。从中可以发现,可以给分类添加实例方法、类方法、协议、以及属性。

接下来看一下分类在运行时的加载调用栈:

1 _objc_init

2 map_3_images

3 map_images_nolock

4 _read_images

5 remethodizeClass

事实上,分类在运行时加载的入口是从remethodizeClass的方法开始的。该方法的功能是:附加外部的分类到已经存在的类里面;修改方法列表、协议列表、属性列表,更新方法缓存以及其子类。

我们先来看一下这个方法的内部实现,然后一步步的探讨系统是如何给宿主类添加分类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;

runtimeLock.assertWriting();

isMeta = cls->isMetaClass();

// Re-methodizing: check for more categories
// 获取cls中未完成整合的所有分类
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);
}
}

方法中的isMeta变量是用来判断我们的分类中是添加的类方法,还是实例方法;

if语句中的unattachedCategoriesForClass方法就是用来获取cls中没有被附加的分类列表。

分支里面的attachCategories方法就是用来附加分类的方法列表、属性列表、协议列表到宿主类中的核心方法。

接着看attachCategories方法的内部实现:

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
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_t,menthod_t,...],[method_t],[method_t,method_t],...]
*/
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);
if (proplist) {
proplists[propcount++] = proplist;
}

protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 获取宿主类当中的rw 数据,其中包含宿主类的方法列表信息
auto rw = cls->data();

// 主要是针对 分类中有关于内存管理相关方法情况下的 一些特殊处理
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);

/*
rw 代表类
methods 代表类的方法列表
attachLists 方法的含义是 将mcount个元素的mlists 拼接到rw的methods上
*/
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);
}

isMeta变量的含义同上文,依然是判断添加的方法是实例方法还是类方法。这里我们只讨论为分类添加实例方法的情况,mlistsproplistsprotolists这三个都是二维数组,分别代表方法列表、属性列表、协议列表。我这里主要分析关于方法添加的逻辑。method_list_t结构存储的是method_t结构这里它代表一个方法,关于这两个的具体内容大家可以去runtime源码里面找答案。

接下来是代表方法数量的mcount变量,属性数量的propcount变量,协议数量的protocount变量;i指的是宿主类分类的总数。

接下来可以看到是一个倒序的while循环,倒序的含义就是最先访问最后编译的分类,因为cats中的分类是按照编译顺序添加的。

这就抛出来一个问题:如果一个类有两个分类,这两个分类有一个同名方法,那么这两个方法哪个会生效?

答案就是取决于两个方法所属的分类的编译顺序,就是说看两个分类哪一个最后被加入到cats中。

经过一系列的处理之后呢,就会执行method.attachLists方法来将分类的方法列表添加到宿主类中。

接下来看一下method.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
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
/*
addedLists 传递过来的二维数组
[[method_t,method_t,...], [method_t], [method_t,method_t,method_t],...]
------------------------ ----------- ---------------------------------
分类A中的方法列表 B C
addedCount = 3
*/
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]));
/*
内存拷贝
[
A ----> [addedLists中的第一个元素],
B ----> [addedLists中的第二个元素],
C ----> [addedLists中的第三个元素],
[原有的第一个元素],
[原有的第二个元素]
]
这也是分类方法会“覆盖”宿主类的方法的原因
*/
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]));
}
}

addedLists是经过处理的方法列表。oldCount是原有元素总数。newCount是拼接之后的元素总数,realloc会重新按照拼接后的总数重新分配空间。然后重新设置元素总数。接下来调用memove移动内存,会将原来的元素向后移动,新拼接的元素会插入到原有元素的前面。最后,调用memcpy拷贝内存,拷贝后的内存存储结构如注释中所示。这也就是分类方法会“覆盖“宿主类方法的原因。

到此为止,分类中的方法已经拼接到宿主类中了。

谢谢您的支持!