Category 原理

OC 中的 Category 是一个十分强大的特性,它可以给一个已有的类添加新的方法或是替换掉已有方法的实现。OC 是一门动态语言从这个功能就可见一斑

猜想

OC 的动态特性来源于 Runtime;所以 Category 是通过 Runtimeclass_addMethod 将方法添加到类里面或替换已有方法的吗?

带着这个疑问,我创建了一个类,然后通过 class_addMethod 尝试往里面添加了一个已存在的方法名,结果返回了 NO。添加失败!!!

看了下这个方法的文档:YES if the method was added successfully, otherwise NO (for example, the class already contains a method implementation with that name). 如果已有同名方法的实现是不能添加的

看来 Category 的实现并没有这么简单,同时在 runtime 中发现了 Category 的结构体定义

1
2
3
4
5
6
7
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}

由此可知,每个 Category 都是一个单独的数据结构,它与对应的类是通过保存的类名联系起来的。然后在 Runtime 中的某个时机将 Category 中的方法拷贝到对应的类中

Runtime 中的实现

Runtime 源代码中可以找到具体的拷贝时间和实现

Category 的注入时间

Category 的处理逻辑在 _read_images 方法中,是通过 map_images_nolock 方法调用的。map_images_nolock 方法是在 dyldmapping 给定的 images 时调用,随即会调用类的 +(void)load 方法

可以知道 Category 的注入发生在初始化 Runtime 的时候,并且在类的 +(void)load 方法调用之前完成

Category 注入逻辑

objc-runtime-new.mm 文件中 // Discover categories. 注释后的代码块)

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
// Process this category.
if (cls->isStubClass()) {
// Stub classes are never realized. Stub classes
// don't know their metaclass until they're
// initialized, so we have to add categories with
// class methods or properties to the stub itself.
// methodizeClass() will find them and add them to
// the metaclass as appropriate.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}

if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}

去掉各种异常处理逻辑后,关于 Category 的实现逻辑就是上面这部分;只要是没有 realized 的类,都调用 objc::unattachedCategories.addForClass 方法将 Category 暂存起来;否则就直接通过 attachCategories 方法将 Category 中的 property / protocol / method 添加到对应的类中

attachCategories

attachCategories 方法会按照 Category 列表,依次将它的方法、属性、协议添加到类中

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
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]));
}
}

总结

添加一个 Category 分为两个阶段:

  1. 编译阶段:将每一个分类都生成一个单独的结构体,保存对应的信息
  2. Runtime 阶段:将编译时生成的 Category 数据在类初始化之前合并到对应的类中

由于在扩充数组的时候 会将类中已存在的方法(属性、协议)先移动到后面,将分类的方法(属性、协议)放在前面,所以分类的数据会被优先调用

如果一个类的多个分类都实现了同一个方法,那么该方法的调用顺序就会取决于编译的顺序,后编译的分类的方法会被先调用。所以尽量避免多个分类实现同一个方法;万不得已时可以通过在 Xcode Build Phases --> compile Sources 中改变文件的编译顺序来控制分类方法的调用

参考

Runtime 源代码

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2017-2021 HonQi

请我喝杯咖啡吧~