运行时之二:分类

由于需求的变化,要为原有类扩展新功能,我们一般有两个方法:继承和组合。而category就是组合的一种具体实现技术。在Objective-C 2.0中,提供了category这个语言特性,可以动态地为已有类添加新行为。

分类功能及特点

我们使用分类可以做哪些事情呢?

  • 扩展已有类(为已存在的类添加方法、属性、协议等)
  • 分解体积庞大的类文件(减少单个文件体积、不同功能组织到不同category中、按需加载想要的category)
  • 声明私有方法(只在需要的地方引入分类,即可调用分类中的方法,不引用该分类的就无法调用(重写原类方法的除外))

  • 模拟多继承(分类中声明别的类中的方法,不实现,通过消息转发实现调用)

  • 把Framework的私有方法公开化(只要知道Framework中私有方法的声明,那么可以在分类中声明Framework中的私有方法,这样可以在需要的地方引入这个分类,就可以调用到Framework中的私有方法了)

分类的特点:

  • 运行时决议(分类编写好后,并没有为宿主类添加上分类的内容,而是在运行时通过runtime,将分类中添加的内容,添加到宿主类中)
  • 可为系统类添加分类

分类中可添加的内容:

  • 实例方法
  • 类方法
  • 协议
  • 实例属性
  • 类属性(形如:@property (nonatomic, strong, class,readonly) NSString *classProperty;)

分类结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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; //属性列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties; //类属性

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *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
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;

runtimeLock.assertWriting();

isMeta = cls->isMetaClass();

// Re-methodizing: check for more categories
//获取cls中未完成整合的所有分类
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
//将分类cats拼接到cls上
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}

static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

/* 二位数组
[[method_t,method_t,method_t,...],[method_t],[method_t,method_t,method_t,...]]
*/
// 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;
/*
当在Build Phases中Compile Sources的自上而下的顺序是[LeoLiuTest LeoliuTest1]时,即编译顺序为[LeoliuTest LeoliuTest1] 则分类的访问顺序就为[LeoliuTest1 LeoliuTest]
这样的一个直观表现既是 如果分类LeoliuTest和LeoliuTest1都有方法methodA,则只会调用LeoliuTest1中的methodA,有“覆盖”的效果
*/
while (i--) {//这里是倒序遍历,最先访问最后编译的分类
//获取一个分类
auto& entry = cats->list[i];

//获取该分类的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);

// =============自己添加打印信息=======
if (strcmp(class_getName(cls), "NSObject") == 0 ) {
printf("leoliu===分类名称:%s==%s\n",class_getName(cls),entry.cat->name);
printf("======================\n");
}
// ===========================
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;
}
}

//获取宿主类当中的rw数据,其中包含宿主类的方法列表信息
auto rw = cls->data();

//主要针对 分类中有关于内存管理相关方法情况下的一些特殊处理
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
/*
rw代表类
methods代表类的方法列表
attachlists 方法的含义是 将含有mcount个元素的mlists拼接到rw的methods上
*/
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);
}

/*
addedLists 传递过来的二位数组
[[method_t,method_t,method_t,...],[method_t],[method_t,method_t,method_t,...]]
--------------------------------- ---------- --------------------------------
分类A中的方法列表(A) B C
addedCount = 3
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
// many lists -> many lists
//列表中原有元素总数 oldCount = 2
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]));
/*
内存拷贝
[
A---> [addedLists中的第一个元素],
B---> [addedLists中的第二个元素],
C---> [addedLists中的第三个元素],
[原有的第一个元素],
[原有的第二个元素]
]
这也是分类方法会“覆盖”宿主类的方法的原因
*/
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]));
}
}

分类源码分析结论:

  • 同名分类方法谁能生效取决于编译顺序(—>attachCategories中的源码可以看出)
  • 分类的添加的方法可以“覆盖”原类方法,并不是真的覆盖,只是分类的方法排在原来方法之前,方法查找时会先查到分类的方法,所以会调用分类的方法,表现上像是原类的方法被“覆盖”了似的

代码验证

1、验证同名分类方法 生效与否与编译顺序的关系

代码结构如图:

三个类中都有方法- (void)methodA,只不过是三个类中打印的内容不同

当编译顺序与打印结果的对照表如下:

编译顺序 打印结果 说明
MyClass+Test1:-[MyClass(Test1) methodA] 后编译的先被访问
MyClass+Test2:-[MyClass(Test2) methodA] 后编译的先被访问
MyClass+Test1:-[MyClass(Test1) methodA] 原类的方法会被分类的直接”覆盖”,与编译顺序无关

2、私有方法公开化

在MyClass.m中定义并实现了一个私有方法

1
2
3
- (void)privateMethod{
NSLog(@"%s",__func__);
}

在MyClass的分类Test1的.h中声明这个私有方法

1
2
3
4
5
6
7
@interface MyClass (Test1)

@property (nonatomic, strong, class,readonly) NSString *classProperty;

- (void)privateMethod;
- (void)methodA;
@end

在想要调用这个私有方法的地方引入#import "MyClass+Test1.h"就能调用到MyClass的私有方法了

1
2
3
4
5
#import "MyClass+Test1.h

{
[[MyClass new] privateMethod];
}

3、+(void)load+(void)initialize的调用顺序

在MyClass及其分类中都添加这两个方法,只不过打印不同

1
2
3
4
5
6
7
8
+(void)load
{
NSLog(@"MyClass:%s",__func__);
}
+(void)initialize
{
NSLog(@"MyClass:%s",__func__);
}

+(void)load方法,文件镜像被读取时调用,直观的表现是程序刚运行就会看到打印结果

编译顺序与打印结果如下:

编译顺序 打印结果 说明
MyClass:+[MyClass load]
MyClass+Test2:+[MyClass(Test2) load]
MyClass+Test1:+[MyClass(Test1) load]
原类最先加载,分类的加载顺序与编译顺序相同
MyClass:+[MyClass load]
MyClass+Test1:+[MyClass(Test1) load]
MyClass+Test2:+[MyClass(Test2) load]
原类最先加载,分类的加载顺序与编译顺序相同
MyClass:+[MyClass load]
MyClass+Test2:+[MyClass(Test2) load]
MyClass+Test1:+[MyClass(Test1) load]
原类最先加载,分类的加载顺序与编译顺序相同

+(void)initialize只有在第一次被使用是会调用一次且仅一次,其他与普通方法相同

编译顺序与打印结果如下:

编译顺序 打印结果 说明
MyClass+Test1:+[MyClass(Test1) initialize] 后编译的先被访问
MyClass+Test1:+[MyClass(Test2) initialize] 后编译的先被访问
MyClass+Test1:+[MyClass(Test1) initialize] 原类的方法会被分类的”覆盖”

QA

Q:

1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
2)、这么些个+load方法,调用顺序是咋样的呢?

A:

1)可以
2)先调用原类的load方法,在调用分类的,分类间的调用顺序根据与编译顺序是相同的

验证:

Xcode中点击Edit Scheme,添加如下两个环境变量:

1
2
3
4
//执行load的方法是打印
OBJC_PRINT_LOAD_METHODS YES
//加载category的时候打印
OBJC_PRINT_REPLACED_METHODS YES


配置如图(更多的环境变量选项可参见objc-private.h)

打印结果如下:

结合当前的编译顺序如下:

所以,对于上面两个问题,答案是很明显的:
1)可以调用,因为附加category到类的工作会先于+load方法的执行
2)+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。

Q:

扩展(Extension)有哪些作用?

A:

  1. 声明私有属性

  2. 声明私有成员变量

  3. 声明私有方法

Q:

分类和扩展的区别是什么?

A:

扩展的特点 分类的特点
编译时决议 运行时决议
只能以声明的形式存在,多数情况下寄生于宿主类的.m中 分类有声明也有实现
不能为系统类添加扩展 可以为系统类添加分类
可以添加实例变量 不可以添加实例变量(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)

参考

深入理解Objective-C:Category

Objective-C 的“多继承”