底层初窥——Category

Category 可以在不修改原来类的基础上为已经存在的类添加方法。这一篇文章会分几个方面来详细介绍 Category。


Category 特点

  • 能在不修改原类的基础上为原类添加实例方法、类方法、协议及属性(这里添加属性仅仅是属性,并不会自动为我们生成实例变量以及 getter 和 setter 方法)。

可以把不同的功能组织到不同的 Category 里面,从而减少单个文件的体积,也更符合单一职责原则。

  • 如果 Category 中的方法和类中原有方法同名,运行时会优先调用 Category 中的方法。也就是,Category 中的方法会覆盖掉类中原有的方法。具体细节,在后面Category 加载过程中会有解释。

  • 如果多个 Category 中存在同名的方法,运行时到底调用哪个方法由编译器决定,最后一个参与编译的方法会被调用。编译顺序可查看 Target -> Build Phases -> Compile Sources,两个分类的顺序决定了 Category 被编译的先后顺序,最后参与编译的 Category 方法会被使用。

对比 extention

  • extension

在编译期决议,它就是类的一部分,在编译期和头文件里的 @interface 以及实现文件里的 @implement 一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension 一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加 extension,所以你无法为系统的类比如 NSString 添加 extension。(详见2)

  • category

在运行期决议。 就 category 和 extension 的区别来看,我们可以推导出一个明显的事实,extension 可以添加实例变量,而 category 是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

Category 的底层实现

这节我们会从编译期 Category 的表现形式来了解其结构,再继续深入 runtime 源码来学习 Category 到底是如何被加载的。

Category 的结构

我们先写一个简单的 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
#import "Person.h"

@interface Person (Addition)<PersonProtocol> // 遵守协议
/* name 属性 */
@property (nonatomic, copy) NSString *personName;
// 类方法
+ (void)printClassName;
// 对象方法
- (void)printName;
@end
------------------------
#import "Person+Addition.h"
@implementation Person (Addition)

+ (void)printClassName {
NSLog(@"printClassName");
}

- (void)printName {
NSLog(@"printName");
}

#pragma mark - <PersonProtocol> 方法
- (void)PersonProtocolMethod {
NSLog(@"PersonProtocolMethod");
}

+ (void)PersonProtocolClassMethod {
NSLog(@"PersonProtocolClassMethod");
}

使用 ==clang -rewrite-objc Person+Addition== 命令转化 Category 实现类后,我们看看 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
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};

static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_Person_Addition_printName},
{(struct objc_selector *)"PersonProtocolMethod", "v16@0:8", (void *)_I_Person_Addition_PersonProtocolMethod}}
};

static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"printClassName", "v16@0:8", (void *)_C_Person_Addition_printClassName},
{(struct objc_selector *)"PersonProtocolClassMethod", "v16@0:8", (void *)_C_Person_Addition_PersonProtocolClassMethod}}
};

static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_PersonProtocol
};

static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"personName","T@\"NSString\",C,N"}}
};

extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;

static struct _category_t _OBJC_$_CATEGORY_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Addition,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Addition,
};
static void OBJC_CATEGORY_SETUP_$_Person_$_Addition(void ) {
_OBJC_$_CATEGORY_Person_$_Addition.cls = &OBJC_CLASS_$_Person;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_Person_$_Addition,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Person_$_Addition,
};
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

一个简单的 Category,突然就变成了这么多的代码,乍一看还真是让人头皮发麻,接下来我们就一个个的来理一下其中的关系。

==_category_t==:Category 结构体

我们知道,所有的 OC 类和对象,在 runtime 层都是用 struct 表示的,category 也不例外。

  • name:类的名字
  • cls:类
  • instance_methods:category 中添加的实例方法列表
  • class_methods:category 中添加的类方法列表
  • protocols:category 中实现的所有协议列表
  • properties:添加的属性

从 Category 的定义中也可以看出来,在 Category 中可以添加实例方法、类方法、协议及添加属性,但是不会添加实例变量及其 setter/getter 方法。

objc_class 的定义里,我们可以看到它有一个 objc_ivar_list——实例变量列表,但是在 Category 中并没有,所以其不能生成实例变量。

  • ==OBJC$CATEGORY_Person$_Addition==:Category 实例

构造函数中,将结构体的参数依次传入,并将实例方法、类方法、实现的协议及属性初始化为结构体需要的类型。

1. ==_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition==:生成实例方法列表
2. ==_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition==:生成类方法列表
3. ==_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Addition==:生成实现协议列表
4. ==_OBJC_$_PROP_LIST_Person_$_Addition==:生成属性列表

实例方法列表构造函数中,可以看到有两个实例方法 ==printName== 及 ==PersonProtocolMethod==。这也证实了 Category 添加属性后不会生成实例变量,也不会对这个属性自动生成 setter/getter 方法。

需要注意到的事实就是 category 的名字用来给各种列表以及后面的 category 结构体本身命名,而且有 static 来修饰,所以在同一个编译单元里我们的 category 名不能重复,否则会出现编译错误。

  • ==L_OBJC_LABEL_CATEGORY_==:Category 数组

存储在 DATA 字段下的 objc_catlist section中,用于运行期 Category 的加载。如果有多个 Category 的话,数组的长度会对应 Category 的数量。

Category 加载过程

Objective-C 的运行是依赖OC的 runtime 的,而 OC 的 runtime 和其他系统库一样,是 OS X 和 iOS 通过 dyld 动态加载的。

我们就关注 runtime 是如何加载 category 的。

首先我们找到 OC 运行时的入口方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// objc-os.mm 870
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_images, load_images, unmap_image);
}

category 被附加到类上面是在 map_images 的时候发生的,在 new-ABI 的标准下,_objc_init 里面的调用的 map_images 最终会调用 objc-runtime-new.mm 里面的 _read_images 方法,而在 _read_images 方法的结尾。

1
_objc_init ---> map_images ---> map_images_nolock ---> _read_images(加载分类)

在 _read_images 中关于加载 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
// Discover categories. 
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;
}

// 处理当前分类
// 首先,使用目标类注册当前分类
// 然后,如果这个类被实现,则重建累的方法列表
bool classExists = NO;
if (cat->instanceMethods || cat->protocols || cat->instanceProperties)
{
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" : "");
}
}

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,就是上节最后讲的编译器为我们生成的 catrgory_t 数组,见L_OBJC_LABEL_CATEGORY_

这段代码用到的最主要的两个方法:

  • ==addUnattachedCategoryForClass(cat, cls->ISA(), hi);==将未添加到类的分类添加到类中。
  • ==remethodizeClass(cls->ISA());==重建类的方法列表。

通过这个方法,我们就能把 catrgoty 的对象方法、协议、属性添加到类上,将类方法、协议添加到类的 metalcass 上。

下面我们具体看看这两个方法是怎么做的。

addUnattachedCategoryForClass(cat, cls->ISA(), hi)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
header_info *catHeader)
{
runtimeLock.assertLocked();

// 获取存储的所有未添加到类中的分类列表
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;

// 在 cats 列表中找到类 cls 对应的分类
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
// 将新增的分类 cat 添加到 list 中
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
// 将新生成的 list 添加重新插入 cats 中,会覆盖旧的 list
NXMapInsert(cats, cls, list);
}

从代码的执行上可以看到,我们获取到 cls 未添加的分类列表 list,并将新增分类 cat 插入到 list 中,并更新了原 list。

可见这个方法的主要作用就是:==将类 cls 与其未添加的分类 list 做一个关联==。

remethodizeClass(cls->ISA());

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

runtimeLock.assertLocked();

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

上一个方法,将类 cls 与其未添加的分类 list 做一个关联,这一个方法主要就是调用 ==attachCategories(cls, cats, true);== 将未依附分类的列表 cats 附加到 cls 类上。

attachCategories(cls, cats, true);

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
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

// 创建方法列表、属性列表、协议列表,用来存储分类的方法、属性、协议
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; // 记录是否是从 bundle 中取的
while (i--) { // 从后往前依次遍历
auto& entry = cats->list[i]; // 取出当前分类

// 取出分类中的方法列表。如果是元类,取得的是类方法列表;否则取得的是对象方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist; // 将方法列表放入 mlists 方法列表数组中
fromBundle |= entry.hi->isBundle(); // 分类的头部信息中存储了是否是 bundle,将其记住
}

// 取出分类中的属性列表,如果是元类,取得的是 nil
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;
}
}

// 取出当前类 cls 的 class_rw_t 数据
auto rw = cls->data();

// 存储方法、属性、协议数组到 rw 中
// 准备方法列表 mlists 中的方法
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 将新方法列表添加到 rw 中的方法列表中
rw->methods.attachLists(mlists, mcount);
// 释放方法列表 mlists
free(mlists);
// 清除 cls 的缓存列表
if (flush_caches && mcount > 0) flushCaches(cls);

// 将新属性列表添加到 rw 中的属性列表中
rw->properties.attachLists(proplists, propcount);
// 释放属性列表
free(proplists);

// 将新协议列表添加到 rw 中的协议列表中
rw->protocols.attachLists(protolists, protocount);
// 释放协议列表
free(protolists);
}

==attachCategories(cls, cats, true);== 把所有 category 的方法列表、协议列表及属性列表分别拼成了一个大列表,然后转交给了==attachLists==方法。

Category 方法的覆盖

attachLists(List* const * addedLists, uint32_t addedCount)

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
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
// 原来有(方法、属性、协议)列表
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]));
}
}

从 ==attachLists(List* const * addedLists, uint32_t addedCount)== 代码中,我们可以得到下面的结论:

category 的方法没有 “完全替换掉” 原来类已经有的方法,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA。

category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 category 的方法会 “覆盖” 掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会返回,不会管后面可能还有一样名字的方法。

attachLists(List* const * addedLists, uint32_t addedCount) 列表中用到的内存管理方法

realloc

原型:extern void *realloc(void *mem_address, unsigned int newsize);

说明:改变mem_address所指内存区域的大小为newsize长度。

memmove

原型:extern void *memmove(void *dest, const void *src, unsigned int count);

说明:由src所指内存区域复制count个字节到dest所指内存区域。src和dest所指内存区域可以重叠,但复制后dest内容会被更改。函数返回指向dest的指针。

memcpy

原型:extern void *memcpy(void *dest, void *src, unsigned int count);

说明:由src所指内存区域复制count个字节到dest所指内存区域。src和dest所指内存区域不能重叠。函数返回指向dest的指针。

调用原类中方法

怎么调用到原来类中被 category 覆盖掉的方法?对于这个问题,我们已经知道 category 其实并不是完全替换掉原来类的同名方法,只是 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
// 假设被覆盖的方法名叫 printName。
Class currentClass = [MyClass class];
MyClass *my = [[MyClass alloc] init];

if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method)) encoding:NSUTF8StringEncoding];
if ([@"printName" isEqualToString:methodName]) {
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
}
}
typedef void (*fn)(id,SEL);

if (lastImp != NULL) {
fn f = (fn) lastImp;
f(my, lastSel);
}
free(methodList);
}

Category 和+load方法

有关 +load 方法的执行顺序问题,可以先看看我另一篇文章——底层初窥——load及initialize之后,再来回答下方的问题。

  • 在类的 +load 方法调用的时候,我们可以调用 category 中声明的方法么?

答案是可以的,因为 load 方法的调用是在 Category 加载到类中之后才执行的。

Category 关联对象

关联对象的深入理解可查看我的另一篇文章——关联对象详解详细了解。

在 Category 中虽然可以添加属性,但是不会生成对应的成员变量,也不能生成 getter、setter 方法。因此,在调用 Category 中声明的属性时会报错。

那么 Category 中声明的属性我们该怎样来使用它呢?

我们可以自己来实现 getter、setter 方法,并借助关联对象(Objective-C Associated Objects)来实现 getter、setter 方法。关联对象能够帮助我们在运行时阶段将任意的属性关联到一个对象上。具体需要用到以下几个方法:

1
2
3
4
5
6
7
8
// 1. 通过 key : value 的形式给对象 object 设置关联属性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 2. 通过 key 获取关联的属性 object
id objc_getAssociatedObject(id object, const void *key);

// 3. 移除对象所关联的属性
void objc_removeAssociatedObjects(id object);

实践关联对象

就直接在上文中演示代码中直接设置关联对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "Person+Addition.h"
#import <objc/runtime.h>

static NSString *nameKey = @"nameKey";

@implementation Person (Addition)

/**
setter方法
*/
- (void)setPersonName:(NSString *)personName {
objc_setAssociatedObject(self, &nameKey, personName, OBJC_ASSOCIATION_COPY);
}
/**
getter方法
*/
- (NSString *)personName {
return objc_getAssociatedObject(self, &nameKey);
}

... 省略一些方法

@end