前言

系列第三篇:深入分析OC中的分类 - Category 以及在分类里面会涉及到了 关联对象

Category

category_t

先来看看category的真面目:category_t

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

category_t结构体定义在objc-runtime-new.h中,可以看出该结构体中含有:实例方法列表、类方法列表、协议列表、属性列表。

编译之后,runtime之前

来看看NSObject的分类:

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface NSObject (vc)

- (void)test;

@end

在编译结束之后,runtime运行之前。NSObject被编译成一个objc_class 结构体;而NSObject (vc) 被编译成一个category_t 结构体。

打印出这个结构体:(在attachCategories方法中判断当前的category位vc,并进入断点)

break point

打印出的category结果如下:

category

runtime之后

运行attachCategories 方法之后,会将category中的所有方法、属性、协议添加到类的class_rw_t中。

我们以添加方法为例,在添加任何的分类方法之前,NSObject的class_rw_t中的methods数组只有一个元素,也就是class_ro_t中的baseMethodList列表。添加一个分类之后,会往methods数组中添加一个元素,该元素的类型是entsize_list_tt

使用lldb查看,发现在添加vc分类后,NSObject里面已经添加了64个分类了;加上baseMethodListmethods数组已经有65个元素。(后添加的分类在数组中的位置靠前,baseMethodList元素在最后面

category

例外

对于系统自带的类,才会在runtime时加载分类;但是对于自己创建的类,在编译结束之后就直接将分类的方法添加到baseMethodList中了,就好像没有分类一说。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// TestOBJ
@interface TestOBJ : NSObject {
NSInteger ivatInt;
}
@property (nonatomic, assign) NSInteger propertyInt;
- (void)test;
@end

// TestOBJ+vc
#import "TestOBJ.h"
@interface TestOBJ (vc)
@property (nonatomic, assign) NSInteger vc;
- (void)test;
@end

_objc_init里面打上断点,这个方法是runtime的入口函数。在这个时候使用lldb,就发现在TestOBJ的baseMethodList里面有2个test方法了,当然分类的test方法在前面,也就是会被先检索到。

关联对象

观察发现,category里面有属性,但是没有实例变量。在系列上一篇Objective-C runtime - 属性与方法了解到,属性没什么用,真正存放的还是实例变量。

那么为什么不能添加实例变量呢? 因为实例变量在每个对象中都有一份。对于使用分类之前,对象的内存是如此分布的;如果添加了分类,那么每个对象的内存结构都需要改变。而对于方法来说,只需要类的结构体来存储方法,类的结构体也就只有一个,就可以随意更改。

那么为什么可以添加属性呢? 只添加属性不添加实例变量,毫无意义。正是因为关联对象的引入,使得这个做法有意义起来。因为关联对象的作用和实例变量相似

源码分析

关联对象相关的源码全部放在objc-reference.hobjc-reference.mm 两个文件中。

关联对象和对象是一一对应的,而不是和类一一对应的

  1. 所有的关联对象都由一个AssociationsManager对象来管理,这个对象里面有一个AssociationsHashMap
  2. AssociationsHashMap由许多key-value构成。key是对象的地址;value是一个ObjectAssociationMap,也就是所谓的关联对象。
  3. ObjectAssociationMap就是关联对象,每个关联对象里面包含多个key-value对。key是属性名;value是ObjcAssociation,也就是相当于属性对应的实例变量。
  4. ObjcAssociation相当于实例变量,该结构体有两个成员:**_policy** 属性的内存管理语义;**_value** 属性的值

association

总结

  1. category可以给既有类添加属性、方法、协议,但是不能添加实例变量
  2. 添加属性对应的实例变量可以使用关联对象来实现
  3. category里面的方法不会覆盖原有的同名方法,只是会被提前检索到。所以感官上觉得被覆盖掉了
  4. 对于系统自带的类,在runtime时才会加载category;但是自定义类的分类在编译期就已经全部加载了,就像不存在分类,但是存在一个大大的自定义类一样。

参考