Category与Extention

category(分类)和 extension(类扩展)是日常开发中经常使用的,下面对它们进行对比分析。



Category (分类)

Category 使用场景

  • 为一个类添加新的方法,可以为系统的类扩展功能
  • 将一个类的不同功能,拆解成多个 category,减少单个文件的体积
  • 创建对私有方法的前向引用: 声明私有方法,把 Framework 的私有方法公开等。直接调用其他类的私有方法时编译器会报错的,这时候可以创建一个该类的分类,在分类中声明这些私有方法(不必提供方法实现),接着导入这个分类的头文件就可以正常调用这些私有方法。
  • 模拟多继承(另外使用 protocol 也可以模拟多继承)

Category 中可以添加哪些内容

可以添加实例方法、类方法、协议、属性(只生成 setter 和 getter 方法的声明,不会生成 setter 和 getter 方法的实现以及下划线成员变量)

Category 编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,这时候分类中的数据还没有合并到类中,而是在程序运行的时候通过Runtime机制将所有分类数据合并到类(类对象、元类对象)中去。这是分类最大的特点,也是分类和类扩展的最大区别,类扩展是在编译的时候就将所有数据都合并到类中去了。

OC中类的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

编译之后,objc_class 结构体的内存布局已经固定,不可能再向这个结构体中添加数据。 ivars 是一个结构体指针,指向的是一个固定区域,无法修改。不过 methodList 是指针的指针,即指针变量当中存的是一个地址,你可以改变这个地址的值从而改变最终指向的变量。所以可以修改 *methodList 的值来增加方法。因此,可以在运行时动态添加方法,不能添加成员变量。但可以添加关联对象。

Category 实现原理

通过 Clang 将以下分类代码转换为 C++ 代码,来分析分类的底层实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//文件:Message+Test.h

#import "Message.h"

NS_ASSUME_NONNULL_BEGIN

@interface Message (Test)<NSCopying>
@property (nonatomic, assign) unsigned long long msgId;
@property (nonatomic, copy) NSString *content;

+ (Message *)create;
- (void)send;

@end

NS_ASSUME_NONNULL_END
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//文件:Message+Test.m

#import "Message+Test.h"

@implementation Message (Test)
@dynamic msgId;
@dynamic content;

+ (Message *)create {
return [[Message alloc] init];
}

- (void)send {
NSLog(@"send message.");
}

@end

执行如下命令进行编译:

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Message+Test.m

得到编译后的文件 Message+Test.cpp,从中选取部分源码如下:

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
79
80
81
82
83
84
85
86
87
88
89
90
// 分类结构体
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[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Message_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"send", "v16@0:8", (void *)_I_Message_Test_send}}
};

// 类方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Message_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"create", "@16@0:8", (void *)_C_Message_Test_create}}
};

// NSCopying 协议实例方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};

struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
0,
"NSCopying",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;

// 协议列表
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Message_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};

// 属性列表
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Message_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"msgId","TQ,D,N"},
{"content","T@\"NSString\",C,D,N"}}
};


// Message+Test 分类编译后的结构如下:
static struct _category_t _OBJC_$_CATEGORY_Message_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Message",
0, // &OBJC_CLASS_$_Message,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Message_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Message_$_Test,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Message_$_Test,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Message_$_Test,
};

从上可知,分类编译之后的底层结构体是 struct category_t,分类中可以添加实例方法、类方法、协议、属性,但是不能添加成员变量,因为没有存储成员变量对应的指针变量。


分类内容加载过程

在编译时,Category 中的数据还没有合并到类中,而是在程序运行的时候通过Runtime机制将所有分类数据合并到类(类对象、元类对象)中去。下面我们来看一下 Category 的加载处理过程:

  1. 通过 Runtime 加载某个类的所有 Category 数据.
  2. 把所有的分类数据(方法、属性、协议),合并到一个数组中.(后面参与编译的 Category 数据,会排在数组的前面)
  3. 将合并后的分类数据(方法、属性、协议),插入到宿主类原来数据的前面.(所以会优先调用最后参与编译的分类中的同名方法)


Extension 类扩展

Extension 有一种说法叫“匿名分类”,因为它很像分类,但没有分类名。它的作用是将原来放在 .h 中的数据放到 .m 中去,私有化,变成私有的声明。

Extension 是在编译的时候就将所有数据都合并到类中去了(编译时决议),而 Category 是在程序运行的时候通过Runtime机制将所有数据合并到类中去(运行时决议)。

类扩展一般用于为类添加私有属性,私有方法,私有成员变量。

Category Extension
运行时决议 编译时决议
可以有声明,可以有实现 只以声明的形式存在,多数情况下寄生于宿主类的.m中
可以为系统的类添加分类 不能为系统类添加扩展


相关面试题

Q:Category 能否添加成员变量?如果可以,如何给 Category 添加成员变量?

因为分类底层结构的限制,不能直接给 Category 添加成员变量,但是可以通过关联对象间接实现 Category 有成员变量的效果。

Q:为什么分类中属性不会自动生成 setter、getter 方法的实现,不会生成成员变量,也不能添加成员变量?

因为类的内存布局在编译的时候会确定,但是分类是在运行时才加载,在运行时Runtime会将分类的数据,合并到宿主类中。

Q:为什么将以前的方法列表挪动到新的位置用 memmove 呢?
为了保证挪动数据的完整性。而将分类的方法列表合并进来,不用考虑被覆盖的问题,所以用 memmove 就好。

Q:为什么优先调用最后编译的分类的方法?

attachCategories()方法中,从所有未完成整合的分类取出分类的过程是倒序遍历,最先访问最后编译的分类。然后获取该分类中的方法等列表,添加到二维数组中,所以最后编译的分类中的数据最先加到分类二维数组中,最后插入到宿主类的方法列表前面。而消息传递过程中优先查找宿主类中靠前的元素,找到同名方法就进行调用,所以优先调用最后编译的分类的方法。

Q:objc_class 结构体中的 baseMethodList 和 methods 方法列表的区别?

回答此道问题需要先了解Runtime的数据结构objc_class。
baseMethodList基础的方法列表,是只读的,不可修改,可以看成是合并分类方法列表前的methods的拷贝;而methods是可读写的,将来运行时要合并分类方法列表。

Q:Category 中有 +load 方法吗?+load 方法是什么时候调用的?+load 方法能继承吗?

分类中有+load方法;
+load方法在Runtime加载类、分类的时候调用;
+load方法可以继承,但是一般情况下不会手动去调用+load方法,都是让系统自动调用。