0%

OC Category浅析

category用结构体category_t表示(在objc-runtime-new.h中可以找到此定义):

1
2
3
4
5
6
7
8
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;
};

而类的结构体:

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

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

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

typedef struct objc_class *Class;

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

MyClass.h

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface MyClass : NSObject

- (void)printName;

@end

@interface MyClass(MyAddition)

@property(nonatomic, copy) NSString *name;

- (void)printName;

@end

MyClass.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation MyClass

- (void)printName
{
NSLog(@"%@", @"MyClass");
}

@end

@implementation MyClass(MyAddition)

- (void)printName
{
NSLog(@"%@", @"MyAddition");
}

@end

转换为.cpp文件:clang -rewrite-objc MyClass.m

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
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_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_MyClass_MyAddition_printName}}
};

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_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"name","T@\"NSString\",C,N"}}
};

extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_MyClass;

static struct _category_t _OBJC_$_CATEGORY_MyClass_$_MyAddition __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MyClass",
0, // &OBJC_CLASS_$_MyClass,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition,
0,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MyClass_$_MyAddition,
};
static void OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition(void ) {
_OBJC_$_CATEGORY_MyClass_$_MyAddition.cls = &OBJC_CLASS_$_MyClass;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_MyClass_$_MyAddition,
};
static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
&OBJC_CLASS_$_MyClass,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_MyClass_$_MyAddition,
};
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

多个category的情况:

1
2
3
4
5
6
7
static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
&OBJC_CLASS_$_MyClass,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [2] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_MyClass_$_MyAddition1,
&_OBJC_$_CATEGORY_MyClass_$_MyAddition2,
};

category的加载

先拿到类的类别数组列表[cat1, cat2, cat3],这个数组元素的顺序是根据编译顺序得来.然后倒序拼装得到一个方法数组列表.最后该方法列表再追加类自身的方法.类别加载结束.因此当有方法覆盖时,执行的将是最后一个编译的类别里的方法.

在类和category中都可以有+load方法,那么有两个问题:

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

A:可以调用,因为附加category到类的工作会先于+load方法的执行.

1
2
3
4
5
6
7
8
9
10
objc[20522]: LOAD: category 'MyClass(MyCategory1)' scheduled for +load
objc[20522]: LOAD: category 'MyClass(MyCategory2)' scheduled for +load
objc[20522]: LOAD: +[MyClass(MyCategory1) load]

2018-01-17 16:28:01.451 categoryDemo[20522:194333] MyCategory1:<MyClass: 0x7f9422c01c30>
2018-01-17 16:28:01.452 categoryDemo[20522:194333] <MyClass: 0x7f9422c01c30>-MyClass,小明
objc[20522]: LOAD: +[MyClass(MyCategory2) load]

2018-01-17 16:28:01.453 categoryDemo[20522:194333] MyCategory2:<MyClass: 0x7f9422f00100>
2018-01-17 16:28:01.453 categoryDemo[20522:194333] <MyClass: 0x7f9422f00100>-MyClass,小刚

+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。

怎么调用到原来类中被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
Class currentClass = [MyClass class];
MyClass *my = [[MyClass alloc] init];
my.name = @"无脑刚";
if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method)) encoding:NSUTF8StringEncoding];
NSLog(@"方法:%@", methodName);
if ([@"printName" isEqualToString:methodName]) {
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
}
}
typedef void (*fn)(id,SEL);

if (lastImp != NULL) {
fn f = (fn)lastImp;
f(my,lastSel);
}
free(methodList);
}

打印如下:

1
2
3
4
5
6
7
8
9
10
2018-01-17 16:51:56.436 categoryDemo[20716:206487] 方法:printName
2018-01-17 16:51:56.436 categoryDemo[20716:206487] 方法:introduceYouselfForCategory2
2018-01-17 16:51:56.437 categoryDemo[20716:206487] 方法:printName
2018-01-17 16:51:56.437 categoryDemo[20716:206487] 方法:introduceYouselfForCategory1
2018-01-17 16:51:56.437 categoryDemo[20716:206487] 方法:printName
2018-01-17 16:51:56.437 categoryDemo[20716:206487] 方法:.cxx_destruct
2018-01-17 16:51:56.437 categoryDemo[20716:206487] 方法:name
2018-01-17 16:51:56.437 categoryDemo[20716:206487] 方法:setName:
2018-01-17 16:51:56.438 categoryDemo[20716:206487] <MyClass: 0x7f8a0d618f40>-MyClass,无脑刚

可以看到调用到了类自身的方法.

在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值,这个时候可以求助关联对象来实现。

但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?

所有的关联对象都由AssociationsManager管理.
AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对.

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy)部分实现:

1
2
3
4
5
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
_class_setInstancesHaveAssociatedObjects(_object_getClass(object));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}

return obj;
}

runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

关于Method,IMP,SEL,self,_cmd

struct objc_method

在runtime.h里有申明.

1
2
3
4
5
6
7
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}

typedef struct objc_method *Method;

附录:

苹果开源网址

objc category的秘密

深入理解Objective-C:Category

macOS 10.13,objc4/objc4-723版本已经找不到attachMethodLists()的实现了.
static void attachMethodLists(Class cls, method_list_t **addedLists, int addedCount, bool baseMethods, bool methodsFromBundle, bool flushCaches)
得看旧版:macOS 10.10,objc4/objc4-646版本

觉得文章有帮助可以打赏一下哦!