Category探究

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方法

编译时不会出错,但是运行时就会产生错误:
在这里插入图片描述
我们分析分析:

  1. 可以调用p.age=10,和打印p.age两句,说明系统已经生成了setter和getter方法的声明
  2. 运行时,又会说找不到setAge:和age方法,说明系统没有实现setter和getter方法
  3. 直接调用_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.将合并后的分类数据(方法、属性、协议),插入到原来数据的前面

参考文献

Category底层结构及源码分析
Category

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值