深入分析Category

0x01 前言

​ em…..好像没啥说的,直接撸起袖子干代码。

前言示意图

0x02 举个例子

   首先,我们新建一个Animal类,然后再建一个Animal的分类。

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
// 头文件
@interface Animal : NSObject

- (void)eat;

@end

@interface Animal (Category)

- (void)sleep;

@end

// 内部实现文件
@implementation Animal

- (void)eat {

}

@implementation Animal (Category)

- (void)sleep {

}

@end

   用clang命令将Animal转成底层代码看下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Animal.m -o Animal-Arm.cpp

   通过下面解析出来的内容,我们可以知道Category在底层是以_category_t的结构体存在的,分类的实例方法是在这个结构体的instance_methods成员变量内。

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
// Category结构体
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;
};

// 将Category结构体内的instance_methods指定为_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Category
static struct _category_t _OBJC_$_CATEGORY_Animal_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Animal",
0, // &OBJC_CLASS_$_Animal,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Category,
0,
0,
0,
};

// 将_category_t结构的cls指定为Animal类,相当于说明自己是Animal的分类
static void OBJC_CATEGORY_SETUP_$_Animal_$_Category(void ) {
_OBJC_$_CATEGORY_Animal_$_Category.cls = &OBJC_CLASS_$_Animal;
}

// 前面说的_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Category就是这里
// 我们写在分类的sleep方法就在这个结构体内
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_Animal_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"sleep", "v16@0:8", (void *)_I_Animal_Category_sleep}}
};

// 生成一个保存在__Data段下__objc_catlist里的数组,长度为1,如果多个分类,这个数组长度延长
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Animal_$_Category,
};

   以此类推,我们同样可以知道Category里的属性、类方法、协议同样被存放在结构体_category_t内。但是结构里是没有存放变量的成员,所以从结构体也可以知道Category是不可以添加变量的,同样属性的setter和getter也只有声明,没有实现。所以这也解释了为什么分类不会自动生成getter和setter,以及为什么不能添加成员变量。

  上面代码里我们也说过,会在Data段下生成一个保存在objc_catlist里的数组,我们通过Mach-O证实这一点。首先是没有分类的Category的Mach-O文件结构图,我们发现在Data段下没有objc_catlist:

没有Category的Mach-O示意图

  而有分类的Category的Mach-O文件结构图里,明显多出了objc_catlist:

有Category的Mach-O示意图

   那么,Category我们知道是通过runtime在运行时被加载的,那么要继续深入挖掘的话,我们需要通过runtime源码进行了解。

0x03 Category信息的加载

    程序启动的时候很多依赖库会在main函数执行之前被执行。比如Runtime所在的libObjc库,这些库都是统一由dyld进行加载的。Runtime的初始化函数在_objc_init 方法,我们首先看下_objc_init是怎么被执行到的,我们下一个_objc_init符号断点。

objc_init调用示意图

  我们可以看到,首先是dyld动态链接器启动,然后把Mach-O加载进来,进行读取操作包括我们的类、分类、方法等,然后libSystem库的初始化,里面包括了libobjc和libdispatch等库,所以libobjc库,也在这一刻初始化。接着看_objc_init方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// 注册dyld事件的监听
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

  map_images对Mach-O中一些符号信息进行初始化。

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
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[]) {
rwlock_writer_t lock(runtimeLock);
// 这个函数最终主要关注在_red_imgaes函数
return map_images_nolock(count, paths, mhdrs);
}

// 读取类信息、协议信息、分类信息等
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
......
// 选取代码片段
......
// 读取Category信息
for (EACH_HEADER) {
// _getObjc2CategoryList下面会展开
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) {
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)
{
// 建立一个映射表。key是类名,value是Category表。因为可能一个类有多个Category
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);
}
}
}
}
......
if (DebugNonFragileIvars) {
// 使得类处于可用状态
realizeAllClasses();
}
......
}

// GETSECT是个宏定义,而__objc_catlist就是前面说的Data段下的
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) { \
return getDataSection<type>(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
}

  这里要讲下,如何重建类的结构,也就是 class_rw_t这个结构体。通过前插的方法,将分类的方法、属性、协议信息加入到类原本的方法、属性、协议结构中。

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
// 重建类的结构
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;

runtimeLock.assertWriting();

isMeta = cls->isMetaClass();

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

// 这个函数很好理解,取出分类的属性、协议、方法信息。通过attachLists函数附加到类原本的结构里
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));

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();
// 将拿到的Category信息,附加到类结构中,如何附加在attachLists函数中
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);
}

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;
// 先将原来的结构移动出足够空间。
// 移动是一种前插行为,比如要添加10个方法进来,就需要将原来结构全部后移出10个位置,这样前面10个位置就空出来了
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) {
list = addedLists[0];
}
else {
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]));
}
}

  因为是通过前插来调整类的结构信息,所以举个例子,比如分类中如果有跟类中一样的方法,就被插入到前面去了,执行方法的时候会去查找方法,但会首先找到分类的方法,所以这就是为什么执行的是分类的方法,而不是类里的方法,而这往往会给我们造成“被覆盖”的感觉,其实原来类的这个方法还是存在在类的方法结构中,只是排在后面而已,如果要执行这个类的方法,我们其实也可以做到的。

0x04 Load方法

  回到前面,我们在说_objc_init的时候,map_images里面做了一些准备工作,让类处于待用状态,而我们或许也会注意到旁边还有一个load_images函数,那么这个函数是干嘛的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

environ_init();
tls_init();
static_init();
lock_init();
exception_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

  load_images函数其实就是调用类以及分类里面的load方法,因为是在程序启动的时候执行的,所以我们也能证实load方法执行时机最早,且只会执行一次

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
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!hasLoadMethods((const headerType *)mh)) return;

recursive_mutex_locker_t lock(loadMethodLock);

// 找到所有的load方法,先找类的,保存到loadable_classes表内;再找分类的,保存到loadable_categories表内。
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}

// 调用load方法
call_load_methods();
}

void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {

while (loadable_classes_used > 0) {
// 遍历loadable_classes表,拿到load方法,然后执行调用执行load方法
// 所以原来类的load方法是早于分类的load方法
call_class_loads();
}
// 遍历loadable_categories表,拿到load方法,然后执行调用执行load方法
more_categories = call_category_loads();

} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

  找到所有的load方法后,就要去执行load方法,这里只拿call_class_loads函数举例,call_category_loads其实是类似的。

  从loadable_classes表中拿到每个类的load方法,然后直接执行。需要注意的是,这里是直接执行,而不是调用消息发送机制来执行方法。

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
static void call_class_loads(void)
{
int i;

struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 拿到load方法
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 直接调用执行,而不是走消息发送机制
(*load_method)(cls, SEL_load);
}

if (classes) free(classes);
}

  正因为是直接执行load方法,不走objc_msgSend消息发送函数,所以也就不存在去方法表中查找方法的过程。所以分类的load方法,并不会”覆盖“原来类的load方法。而且源码里,首先执行的是原来类的load方法,其次执行分类的load方法,所以原来类的load方法执行时间早于分类的load方法

0x05 Initialize 方法

  说到了load方法,就不得不提initialize方法,这两个方法经常拿在一起进行比较,这里也不例外,我们继续通过源码来挖掘什么时候开始执行initialize方法。

  我们在initialize方法打个断点看下调用栈

initialize调用示意图

  我们初始化一个对象的时候,常用的方法是alloc或者new,这两个方法都是通过消息发送来调用的。我们可以看到,在objc_msgSend的汇编代码中,调用了objc_msgSend_uncached。

  在objc_msgSend_uncached里面,调用了MethodTableLookup这个宏

1
2
3
4
5
6
7
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

MethodTableLookup
br x17

END_ENTRY __objc_msgSend_uncached

  MethodTableLookup宏的定义如下:里面我们可以看到跳转到了__class_lookupMethodAndLoadCache3函数函数。

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
.macro MethodTableLookup

// push frame
stp fp, lr, [sp, #-16]!
mov fp, sp

// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]

mov x2, x16
// 跳转到__class_lookupMethodAndLoadCache3函数
bl __class_lookupMethodAndLoadCache3

mov x17, x0

ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]

mov sp, fp
ldp fp, lr, [sp], #16

.endmacro

  _class_lookupMethodAndLoadCache3里面又调用了lookUpImpOrForward函数

1
2
3
4
5
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

  lookUpImpOrForward函数里面入参initialize接收的是YES,而且第一次进来的时候cls->isInitialized()是肯定为NO的。所以只当这个对象第一次调用objc_msgSend的时候肯定会调用_class_initialize函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
......
// bool isInitialized() {
// return getMeta()->data()->flags & RW_INITIALIZED;
// }
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
......
}

  _class_initialize函数里面首先调用父类的initialize方法,然后调用callInitialize函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_initialize(Class cls)
{
......
// 同时调用父类的initialize方法
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
......
{
callInitialize(cls);

if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
}
}

  通过消息调用,执行initialize方法。

1
2
3
4
5
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}

  调用方式来看,我们可以发现load方法是直接执行,而initialize方法是通过消息发送来执行的。所以在这里,我们可以知道有分类实现initialize方法的情况下,只会调用分类的initialize方法,原来的类的initialize方法并不会被执行,而且执行顺序是先执行父类的initialize方法,再执行子类的initialize方法。我们举个例子,新建Animal类、Animal分类、继承Animal的子类Dog、Dog分类。

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
@implementation Animal

+(void)initialize {
NSLog(@"%@", [self description]);
}

@end

@implementation Animal (Category)

+(void)initialize {
NSLog(@"%@-Category", [self description]);
}

@end

@implementation Dog

+(void)initialize {
NSLog(@"%@", [self description]);
}

@end

@implementation Dog (Category)

+(void)initialize {
NSLog(@"%@-Category", [self description]);
}

@end

// 打印结果
2018-05-09 15:35:56.587531+0800 testData[36930:28695275] Animal-Category
2018-05-09 15:35:56.587665+0800 testData[36930:28695275] Dog-Category

  结果可以看到,如果有Category的情况下,都是先执行Category的initialize方法;其次先调用父类的initialize方法,再调用子类的initialize方法。同时,我们可以测试下,如果子类的initialize方法不实现,而只实现父类的initialize方法会有什么效果?

1
2
2018-05-09 15:42:46.513940+0800 testData[37636:28757829] Animal-Category
2018-05-09 15:42:46.514091+0800 testData[37636:28757829] Animal-Category

  父类的的initialize方法被执行了两次,这又是为什么呢?

0x06 isa指针与SuperClass

  我们首先看下Class的结构:

1
typedef struct objc_class *Class;

  指向objc_class结构体:

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
struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;

class_rw_t *data() {
return bits.data();
}
......
};

struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

// 继承自的objc_object里面只有一个isa。
// 所以相当于如下结构
struct objc_class {
Class _Nonnull isa;
Class superclass;
// 调用过的方法会被保存到cache,下次调用的时候先从Cache查找,提高效率
cache_t cache;
class_data_bits_t bits;
// class_rw_t是一个很重要的结构
class_rw_t *data() {
return bits.data();
}
......
};

struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;
// 方法表
method_array_t methods;
// 属性表
property_array_t properties;
// 协议表
protocol_array_t protocols;
......
}

  受益于苹果开源,我们知道现在Class的结构如上面代码所示,而不是很多博客里的说到的这样的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这是Objective-C 1.0的结构
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
const uint8_t *ivar_layout;
struct old_class_ext *ext;
......
};

  所以我们可以看到上面结构中都有一个isa和superClass成员,之间的关系可以通过下面这张图概况。

isa和superClass关系示意图

  那么isa的作用如下:

  • 实例对象的isa指向类

    调用实例方法时,会先通过isa找到类,再在类的方法表中找到这个实例方法进行调用。

  • 类的isa指针指向元类

    调用类方法时,先通过类的isa找到元类,再在元类的方法表中找到这个类方法进行调用。

  同样superClass的作用,显而易见是找到父类,下面会通过例子详细了解调用过程,进行实验前,我们必须记住的是调用实例方法是到这个类的方法表中查找的,实例结构自己是不保存这些的,它只做值的存储,比如age属性,实例结构里面只保存age的值,比如20这样的。而调用类方法是到这个类的元类里的方法表进行查找的。仔细看下上面这张图就可以很好理解了。

Example 实例对象调用自己的方法

  测试代码如下:

1
2
3
4
5
6
7
8
9
@interface Animal : NSObject

@end

@interface Dog : Animal

- (void)eat;

@end

  这个过程是首先实例方法首先通过isa找到它的类对象,然后遍历类对象的方法表,找到eat方法进行调用。

Example 实例对象调用父类的方法

  测试代码如下:

1
2
3
4
5
6
7
8
9
@interface Animal : NSObject

- (void)eat;

@end

@interface Dog : Animal

@end

  这个过程是首先实例方法首先通过isa找到它的类对象,然后遍历类对象的方法表,但是方法表里并没有这个方法,于是通过superClass找到父类对象,再到父类的方法表里进行查找,最后找到eat方法进行调用。假设父类还是没有,再一层一层上去找,直到基类为止,如果还是没找到就报错 unrecognized selector sent 。

Example 类对象调用自己的方法

  测试代码如下:

1
2
3
4
5
6
7
8
9
@interface Animal : NSObject

@end

@interface Dog : Animal

+ (void)eat;

@end

  这个过程是直接在自己的元类方法表里进行查找。同样的,如果是父类的类方法,就调用superClass找到父类对象,再到父类的元类里的方法表进行查找,直到基类元类为止,如果还是没找到就报错 unrecognized selector sent 。

Example 一个特殊情况

  测试代码如下:这个例子里,只有NSObject实现eat的实例方法(OC里NSObject是所有类的基类),而调用方式是[Dog eat]这样的调用类方法,运行后结果如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface NSObject (Category)

- (void)eat;

@end

@interface Animal : NSObject

@end

@interface Dog : Animal

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

[Dog eat];
}

@end

  执行结果是成功运行了,为什么我明明调用的是类方法,而实例方法却被触发了。如果仔细看过上面图的话,我们可以发现,当调用类方法时,元类一层一层往上进行查找,如果基类的元类里面的方法表也没有,就会来到基类(NSObject)里面的方法表进行查找,我们知道类里面保存的是实例方法,所以就会调用test方法成功。

  回到前面留下的问题,Category里没实现initialize方法,而只在类里面实现initialize方法,这个类的initialize方法为什么会被执行两次?回到_class_initialize这个函数里可以看到父类的一次执行是在这里执行的。而第二次是因为自己类的元类方法表里找不到initialize方法,所以去父类的元类方法表里进行查找了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void _class_initialize(Class cls)
{
......
// 父类的initialize方法被执行一次
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
......
{
callInitialize(cls);

if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
}
}

0x07 objc_getAssociatedObject和objc_setAssociatedObject

  我们知道分类里面可以添加property属性,但不会生成getter、settter方法和实例变量。所以给属性设置和获取值是通过objc_setAssociatedObject和objc_getAssociatedObject实现的。那么为何一定要通过这种方式实现,比如下面的代码会有什么问题?我们通过全局变量来控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface Animal (Category)

@property (nonatomic, assign) int age;

@end


// 定义全局变量
int g_Age = 0;

@implementation Animal (Category)

- (void)setAge:(int)age {
g_Age = age;
}

- (int)age {
return g_Age;
}

@end

  显然是有问题的,全局变量共用的是一份,如果创建多份实例,去修改这个g_Age,那么每个实例对象的age属性值不具备唯一性,我们通过测试代码看下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
    Animal *a = [[Animal alloc] init];
a.age = 10;

NSLog(@"生成b对象前,a对象的年龄是%@岁", @(a.age));

Animal *b = [[Animal alloc] init];
b.age = 15;

NSLog(@"生成b对象后,a对象的年龄是%@岁", @(a.age));

// 运行结果:我们发现对象a的age,被对象b改掉了
2018-05-09 18:16:11.724830+0800 testData[4751:54320510] 生成b对象前,a对象的年龄是10
2018-05-09 18:16:11.725054+0800 testData[4751:54320510] 生成b对象后,a对象的年龄是15

  所以上面这种方案肯定不行的。说到唯一性,肯定还会想到每个实例对象肯定都不同的,如果以对象为key的字典,那么就能确保唯一性,比如下面这样的。

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
@interface Animal (Category)

@property (nonatomic, assign) int age;

@end

// 定义一份全局字典
NSMutableDictionary *g_AgeDict;
@implementation Animal (Category)

+(void)load {
g_AgeDict = [NSMutableDictionary dictionary];
}

- (void)setAge:(int)age {
[g_AgeDict setValue:@(age) forKey:[NSString stringWithFormat:@"%p", self]];
}

- (int)age {
NSNumber *age = [g_AgeDict objectForKey:[NSString stringWithFormat:@"%p", self]];
return age.intValue;
}

@end

// 测试代码
Animal *a = [[Animal alloc] init];
a.age = 10;

NSLog(@"生成b对象前,a对象的年龄是%@岁", @(a.age));

Animal *b = [[Animal alloc] init];
b.age = 15;

NSLog(@"生成b对象后,a对象的年龄是%@岁", @(a.age));

// 运行结果:
2018-05-09 18:30:49.964833+0800 testData[6293:54441445] 生成b对象前,a对象的年龄是10
2018-05-09 18:30:49.965028+0800 testData[6293:54441445] 生成b对象后,a对象的年龄是10

  我们根据运行结果可以看到我们保证了唯一性,但是缺点也很明显,我们如果有多个属性,岂不是要创建很多全局的字典。那么我们又想到可以这样做:

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
@interface Animal (Category)

@property (nonatomic, assign) int age;

@end

NSMutableDictionary *g_Dict;
@implementation Animal (Category)

+(void)load {
g_Dict = [NSMutableDictionary dictionary];
}

- (void)setAge:(int)age {
NSMutableDictionary *dict = [g_Dict objectForKey:[NSString stringWithFormat:@"%p", self]];
if(!dict) dict = [NSMutableDictionary dictionary];

dict[@"age"] = @(age);

[g_Dict setValue:dict forKey:[NSString stringWithFormat:@"%p", self]];
}

- (int)age {
NSMutableDictionary *dict = [g_Dict objectForKey:[NSString stringWithFormat:@"%p", self]];
NSNumber *age = dict[@"age"];
return age.intValue;
}

@end

  虽然可以实现了我们的需求,但是这样写还是麻烦,那我们看看官方怎么做的,先看objc_setAssociatedObject源码:

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
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}

// 里面维护了一份全局关联哈希表,里面一个对象对应一份对象关联表。对象对应的对象关联表里保存的就是我们objc_setAssociatedObject设置的内容。
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}

   所以我们可以明白通过objc_setAssociatedObject设置值,其实就是往一份全局的哈希表中给自己所对应的关联表中以key为健值,设置以value为值的过程。同样objc_getAssociatedObject也是相同步骤,只是换成了取值而已。我们发现,跟我之前自己实现的方案思路是差不多的,但这个显然方便多了。

  category_t结构中是没有存放ivar表的,所以上述操作相当于给对象关联一个成员变量,只是在普通类中这个成员变量在ivar表中,而分类中这个成员变量被维护在一个哈希表中。我们以前经常说如何给分类添加一个属性,所以严谨的来说,我们添加的不是属性,而是手动的生成setter和getter方法,并且维护一个成员变量到一个哈希表中。分类的属性的信息是可以被存到分类的_prop_list_t表中的。

  看完源码,我们再看下使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
static const void *ageKey = &ageKey;
@implementation Animal (Category)

- (void)setAge:(int)age {
objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)age {
NSNumber *age = objc_getAssociatedObject(self, ageKey);
return age.intValue;
}

@end

  首先看下第三个参数objc_AssociationPolicy ,它一共有以下几个值:

  • OBJC_ASSOCIATION_ASSIGN

    相当于@property(nonatomic, assign)

  • OBJC_ASSOCIATION_RETAIN_NONATOMIC

    相当于@property(nonatomic, strong)

  • OBJC_ASSOCIATION_COPY_NONATOMIC

    相当于@property(nonatomic, copy)

  • OBJC_ASSOCIATION_RETAIN

    相当于@property(atomic, strong)

  • OBJC_ASSOCIATION_COPY

    相当于@property(atomic, copy)

  再看下第二个参数key,我们看到需要一个const void *指针,也就是一个任意指针都可以,所以我们精简至如下也是可以的,不用创建那么多key也可以达到我们的需求:

1
2
3
4
5
6
7
8
9
10
11
12
@implementation Animal (Category)

- (void)setAge:(int)age {
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)age {
NSNumber *age = objc_getAssociatedObject(self, _cmd);
return age.intValue;
}

@end