Category结构与原理

分类的实现原理,编译时的表现,运行时的加载原理
参考文档: objc4-723

category的主要作用是为已经存在的类添加方法。可以把类的实现分开在几个不同的文件里面。

Category 结构体

Category用Category_t结构体定义:

1
2
3
4
5
6
7
8
typedef 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;//添加的所有属性
} category_t;

从category的定义也可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。

Category 与 Extension

extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。

但是category则完全不一样,它是在运行期决议的。
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而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
31
32
33
34
35
//.h
#import <Foundation/Foundation.h>

@interface OneClass : NSObject

-(void)oneMethod;

@end

@interface OneClass(OneCategory)

@property(nonatomic, copy) NSString *oneCategoryProp;

-(void)oneCategoryMethod;

@end

//.m
#import "OneClass.h"

@implementation OneClass

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

@end

@implementation OneClass(OneCategory)

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

@end

然后使用Clang命令编译下,命令如下:

1
clang -rewrite-objc OneClass.m

然后得到一个3m多的OneClass.app文件……

在OneClass.app中找到相关的代码片段:

生成了实例方法列表:

1
2
3
4
5
6
7
8
9
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_OneClass_$_OneCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"oneCategoryMethod", "v16@0:8", (void *)_I_OneClass_OneCategory_oneCategoryMethod}}
};

生成了属性列表:

1
2
3
4
5
6
7
8
9
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_OneClass_$_OneCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"oneCategoryProp","T@\"NSString\",C,N"}}
};

生成了category本身 ,并用前面的实例方法列表和属性方法列表初始化

1
2
3
4
5
6
7
8
9
static struct _category_t _OBJC_$_CATEGORY_OneClass_$_OneCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
"OneClass",
0, // &OBJC_CLASS_$_OneClass,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_OneClass_$_OneCategory,
0,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_OneClass_$_OneCategory,
};

最后,编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组L_OBJC_LABELCATEGORY$(当然,如果有多个category,会生成对应长度的数组),用于运行期category的加载。

1
2
3
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_OneClass_$_OneCategory,
};

Category运行期加载

objc_init 和 map_images

在OC运行时入口方法_objc_init中((在objc-os.mm文件中)):

1
2
3
4
5
6
7
void _objc_init(void)
{
……
//category被附加到类上面是在map_images的时候发生的
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
……
}

具体点大概是这么个流程:

1
_objc_init  -> map_images -> map_images_nolock -> _read_images

read_images

_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:

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
// Discover categories. 
for (EACH_HEADER) {
//catlist就是上节中讲到的编译器为我们准备的category_t数组
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;
。。。
continue;
}

// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
// 把category的实例方法、协议以及属性添加到类上
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
。。。
}
//把category的类方法和协议添加到类的metaclass上
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
。。。
}
}
}

从代码中可以看到remethodizeClass处理添加事宜方法

remethodizeClass && attachCategories

既然remethodizeClass才是真正去处理添加事宜方法那么现在看一下remethodizeClass:

1
2
3
4
5
6
7
8
9
10
static void remethodizeClass(Class cls)
{
category_list *cats;
。。。
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
。。。
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}

emmm 好像remethodizeClass也没干什么事,都交给了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
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_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, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}

protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}

auto rw = cls->data();

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

这个函数的主要作用是将 Category 中的方法、属性和协议整合到类(主类或元类)中,更新类的数据字段 data() 中 method_lists(或 method_list)、properties 和 protocols 的值。

  • category的方法没有替换掉原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
  • category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休。
  • 怎么调用到原来类中被category覆盖掉的方法?对于这个问题,我们已经知道category其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,所以我们只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法