什么是Category?
Category是Objective-C 2.0之后添加的语言特性,Category的主要作用是为已经存在的类添加方法,一般称为分类,文件名格式是”NSObject+A.h”。
1 | struct category_t { |
从结构能看出分类可以扩展实例方法列表、类方法列表、协议列表,也支持扩展属性,但不支持扩展成员变量(之后会说)。
一般使用的场景有扩展现有类方法、代码分区、添加私有方法(不对外暴露category.h)、模拟多继承(使用关联对象的方式添加属性实现)
什么是Extension?
Extension一般被称为类扩展、匿名分类,用于定义私有属性和方法,不可被继承。只能依附自定义类写于.m中,定义一般为:
1 | @interface ViewController () |
类扩展支持写在多个.h文件,但都必须在.m文件中引用,且不能有自己的实现。
类扩展很多时候会与分类搞混,我在文后问答环节详细整理了他们的区别。
Category如何加载的?
1 | struct objc_class : objc_object { |
先简单了解一下Class对象的结构,每个objc_class
都包含有class_data_bits_t
数据位,其中储存了class_rw_t
的指针地址和一些其他标记。class_rw_t
中包含有属性方法协议列表,以及class_ro_t
指针地址。而在class_ro_t
结构中,储存的是编译器决定的属性方法协议。
那么是怎么运行的呢?
在编译期类的结构中的class_data_bits_t
指向的是一个 class_ro_t
指针。
在运行时调用realizeClass
方法,初始化一个class_rw_t
结构体,设置ro值为原数据中的class_ro_t
后设为数据位中的指向,最后调用methodizeClass
方法加载。
1 | static void methodizeClass(Class cls) |
可以看到,在methodizeClass
中加载了原先类在编译期决定的方法属性和协议,然后获取了未连接的分类表,将列表中的扩展方法添加到运行期类中。
Category方法覆盖
如果不同的分类实现了相同名字的方法,那么调用时会使用最后加入的实现,这是为什么呢?
加载Category
dyld链接并初始化二进制文件后,交由ImageLoader
读取,接着通知runtime
处理,runtime
调用map_images
解析,然后执行_read_images
分析文件中包含的类和分类。
1 | //加载分类 |
添加方法属性和协议
如果有新增的分类,就分别添加到原类和meta类,并通过remethodizeClass
更新,具体就是调用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
53static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
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));
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();
//加载列表到rw中
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);
}
1 | void attachLists(List* const * addedLists, uint32_t addedCount) { |
可以看到最后调用了rw->methods.attachLists(mlists, mcount);
把新增分类中的方法列表添加到实际运行时查询的方法列表头部。
在进行方法调用时会从头部查询,一旦查到后就返回结果,因此后编译的文件中的方法会被优先调用。
同时之前添加的方法实现也保存了,可以通过获取同名方法的方式查找原类的实现。
Category实现属性
分类不能添加成员变量
属性(Property)包含了成员变量(Ivar)和Setter&Getter。
可以在分类中定义属性,但由于分类是在运行时添加分类属性到类的属性列表中,所以并没有创建对应的成员变量和方法实现。
关联对象
如果我们想让分类实现添加新的属性,一般都通过关联对象的方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 声明文件
@interface TestObject (Category)
@property (nonatomic, strong) NSObject *object;
@end
// 实现文件
static void *const kAssociatedObjectKey = (void *)&kAssociatedObjectKey;
@implementation TestObject (Category)
- (NSObject *)object {
return objc_getAssociatedObject(self, kAssociatedObjectKey);
}
- (void)setObject:(NSObject *)object {
objc_setAssociatedObject(self, kAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
这种方式可以实现存取对象,但是不能获取_object
变量。
问答
分类和扩展有什么区别?
1.分类多用于扩展方法实现,类扩展多用于申明私有变量和方法。
2.类扩展作用在编译期,直接和原类在一起,而分类作用在运行时,加载类的时候动态添加到原类中。
3.类扩展可以定义属性,分类中定义的属性只会申明setter/getter,并没有相关实现和变量。
分类有哪些局限性?
1.分类只能给现有的类加方法或协议,不能添加实例变量(ivar)。
2.分类添加的方法如果与现有的重名,会覆盖原有方法的实现。如果多个分类方法都重名,则根据编译顺序执行最后一个。
分类的结构体里面有哪些成员?
分类结构体包含了分类名,绑定的类,实例与类方法列表,实例与类方法属性以及协议表。