Category的介绍
- Category是Objective-C 2.0之后添加的语言特性。分类、类别其实都是指的Category。
- Category的主要作用是为已经存在的类添加方法。也可以说是将庞大的类代码按逻辑划入几个分区。
- Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
Category的使用
Category的使用在之前这篇文章中已经介绍过了,此处不再赘述
根据Category源码进行剖析
Category 是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
Category的特点
关于添加属性
由Category的结构体可知,结构体中没有属性列表,只有方法列表。也就是说,原则上讲,只能添加方法, 不能添加属性(成员变量)
例如,先创建一个Person属性:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (strong, nonatomic) NSString *name;
@end
NS_ASSUME_NONNULL_END
#import "Person.h"
@implementation Person
@end
给其属性创建一个Age的分类:
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Age)
@property (assign) int age;
@end
NS_ASSUME_NONNULL_END
#import "Person+Age.h"
@implementation Person (Age)
@end
再进行调用:
Person *person = [[Person alloc] init];
person.age = 10; //这一句会调用setter方法
NSLog(@"%i", person.age); //这一句会调用getter方法
编译时不会出错,但是运行时就会产生错误:
我们分析分析:
- 可以调用p.age=10,和打印p.age两句,说明系统已经生成了setter和getter方法的声明
- 运行时,又会说找不到setAge:和age方法,说明系统没有实现setter和getter方法
- 直接调用_age也会报错,说明没有生成成员变量。这样得以证明以上关于分类中属性的结论
那我们加入成员变量,完善setter和getter方法试试
在加入成员变量时又会发现,在分类中不能直接加入成员变量,会报错
回忆一下OC对象的本质,他们的方法列表,属性列表,协议列表都是可读可写的,但是成员变量列表是只读的。也就是说,一个类生成之后,编译时就已经把成员列表信息放在class_ro_t中,不允许再动态的修改,即在分类中也无法再次加入成员变量
总结一下就是,虽然可以添加属性,但是不会自动生成setter和getter方法,也不会自动生成成员变量,只会生成setter和getter方法的声明。这就是为什么在分类中扩展了属性,在外部并没有办法调用。在外部调用点语法设值和取值,本质其实就是调用属性的setter和getter方法,现在系统并没有实现这两个方法,所以外部就没法调用分类中扩展的属性
方法重名
如果分类中有和原有类同名的方法,会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类。同名分类方法生效取决于编译顺序: 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法
以下内容来自:Category底层结构及源码分析
我们重新写一个例子:
// 声明Person类
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, assign) double weight;
- (void)eat;
- (void)run;
@end
#import "Person.h"
@implementation Person
- (void)eat{
NSLog(@"Person - eat");
}
- (void)run{
NSLog(@"Person - run");
}
@end
// Person的类扩展 Person+Eat
#import "Person.h"
@interface Person (Eat)<NSCopying>
@property (nonatomic, assign) int age;
- (void)eat;
- (void)eat1;
+ (void)eat;
@end
#import "Person+Eat.h"
@implementation Person (Eat)
- (void)eat{
NSLog(@"Person(Eat) - eat");
}
- (void)eat1{
NSLog(@"Person(Eat) - eat1");
}
+ (void)eat{
NSLog(@"Person(Eat) + eat");
}
@end
// Person的类扩展Person+Run
#import "Person.h"
@interface Person (Run)
- (void)run;
@end
#import "Person+Run.h"
@implementation Person (Run)
- (void)run{
NSLog(@"Person(Run) - run");
}
@end
// 外部调用
Person *p = [[Person alloc] init];
[p eat];
[p run];
[p eat1];
将上面代码转为C++:
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; // 属性列表
};
接着我们找到_method_list_t结构体(对象方法列表)
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Eat_eat},
{(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_Person_Eat_eat1}}
};
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat
从名称可以看出是INSTANCE_METHODS对象方法,结构体中存储了方法占用的内存,方法数量,以及分类中实现的eat,eat1两个对象方法。
再接着我们找到_method_list_t结构体(类方法列表)
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_C_Person_Eat_eat}}
};
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat
从名称可以看出是CLASS_METHODS类方法,结构体中存储了方法占用的内存,方法数量,以及分类中实现的eat类方法。
再接着我们找到_protocol_list_t结构体(协议列表)
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};
_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat
结构体中存储了协议的数量以及分类遵守的NSCoping协议
_prop_list_t结构体(属性列表)
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_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"age","Ti,N"}}
};
_OBJC_$_PROP_LIST_Person_$_Eat
结构体中存储了属性所占的内存,属性数量以及分类中声明的属性age。
最后我们看到了_OBJC_$_CATEGORY_Person_$_Eat
结构体,我们将上面分析的结构体一一赋值,把两段代码做一下对照
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;
};
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat,
};
接下来我们来到runtime源码,看看运行时又是如何将这些信息同步到类中的。
首先来到runtime的初始化函数,在objc-os.mm文件文件中搜索_objc_init
接着我们来到&map_images
(images代表镜像或者模块),这个函数又会调用map_images_nolock
,接着会调用_read_images
,runtime加载模块的函数,我们找到其中加载category的逻辑代码
这段代码是用来查找项目中的分类的。通过_getObjc2CategoryList
函数获取到项目中每个类的分类列表,进行遍历,获取分类中的方法,协议,属性等信息。最后调用了remethodizeClass
函数,进行类和元类中的重新组织方法。我们进到函数内部查看。
接下来进入到attachCategories
函数内部
这段代码就是取出分类中方法,属性,协议,然后分别拼接到原有类中。拼接时有个小特点,最后加载的分类,即项目中最后编译的分类中的数据,会放在新的数据数组的最前面。具体流程截图中的注释写的很清楚。方法,属性,协议的拼接都是调用的attachLists
函数,接下来我们进入到函数中
上述源码中有两个重要的数组
array()->lists: 类对象原来的方法列表,属性列表,协议列表。
addedLists:传入所有分类的方法列表,属性列表,协议列表。
attachLists
函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝,大概是这么执行的:
那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。其实经过上面的分析我们知道本质上并不是覆盖,而是方法顺序发生了变化,系统会优先调用分类中的方法。
总结一下Category的加载处理过程
1.通过runtime加载某各类的所有Category数据
2.把所有Category的方法、属性、协议数据,合并到一个大数组中(后面参与编译的Category数据会在数组的前面)
3.将合并后的分类数据(方法、属性、协议),插入到原来数据的前面