文章目录
Category
- Category是Objective-C 2.0之后添加的语言特性,分类、类别 其实都是指的Category。
- Category的主要作用是为已经存在的类添加方法。也可以说是将庞大的类代码按逻辑划入几个分区。
- Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
Category的创建
按下面方式创建Category
File Type中选择Category,Class中选择已有类中的所需要模块化的类
如下图是我创建的三个Person类的分类
Category的特点
- 分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。
- 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性
- 分类中的可以写
@property
, 但不会
生成setter / getter
方法, 也不会
生成实现以及私有的成员变量,会编译通过,但是引用变量会报错; - 如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类;
- 同名分类方法生效取决于编译顺序: 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。
分类的实现原理
- 将 Category 和它的主类(或元类)注册到哈希表中;
- 如果主类(或元类)已实现,那么重建它的方法列表。
Category 中的实例方法和属性被整合到主类中,类方法则被整合到元类中;
对协议的处理比较特殊,Category 中的协议被同时整合到了主类和元类中。
最终都是通过调用 staticvoid remethodizeClass(Class cls)
函数来重新整理类的数据的。
Category不能添加成员变量
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; // 属性列表
};
- 从结构体可以知道,有属性列表,所以分类可以声明属性,但是分类只会生成该属性对应的
get
和set
的声明,没有去实现该方法。 - 结构体没有成员变量列表,所以不能声明成员变量。
方法可以运行时改变,结构体不能运行时改变,要想改变原结构体,增加一个属性,只能用关联变量的方式,而关联本质是在类的定义之外为类增加额外的存储空间,是一层映射关系
优点
- 声明私有方法
- 分解体积庞大的类文件
- 把Framework私有方法公开
- 模拟多继承(另外可以模拟多继承的还有
protocol
)
在Category中添加属性
前面提到在Category中无法添加成员变量,但可以添加属性。如果按老方法直接添加属性会怎么样呢?如下
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Per)
@property (nonatomic, strong) NSString* name;
@end
NS_ASSUME_NONNULL_END
如果主函数不涉及到对name
属性的存取操作,编译器是能正常通过的,但如果涉及到了就会报错,如下:
从上面操作可以看出,
- 添加的属性系统并没有自动生成成员变量,也没有实现
set
和get
方法,只是生成了set
和get
方法的声明。这就是为什么在分类中扩展了属性,在外部并没有办法调用。 - 在外部调用点语法设值和取值,本质其实就是调用属性的
set
和get
方法,现在系统并没有实现这两个方法,所以外部就没法调用分类中扩展的属性。
关联对象
associatedObject
又称关联对象。顾名思义,就是把一个对象关联到另外一个对象身上。使两者能够产生联系。目前我能想到的关联对象的使用场景有如下几点:
- 运行时给
cagetory
添加getter
和setter
。因为category
中添加的property
不会生成带下划线"_"的成员变量以及getter
和setter
的实现。所以可以通过关联对象实现getter
和setter
。 - 有时需要在对象中存储一些额外的信息,我们通常会从对象所属的类中继承一个子类。然后给这个子类添加额外的属性,改用这个子类。然而并非所有的情况都能这么做,有时候类的实例可能是由某种机制创建的,而开发者无法另这种机制创建出自己所写的子类实例。此时可以使用“关联对象”。
- 有时只是给某个类添加一个额外的属性,完全没有必要继承出来一个子类。此时可以使用“关联对象”。
delegate
回调的方法中使用关联对象。有时候在一些delegate
回调的方法中需要处理一些回调任务。比如发起网络请求和在delegate
回调的方法中做UI的更新。这样一来,发起网络请求和在回调中更新UI的代码被分散到了两个地方,不利于管理和阅读。此时可以使用“关联对象”。
关联对象相关API
添加关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc AssociationPolicy policy)
获得关联对象
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象
void objic_removeAssociatedObjects(id object)
关联API对象参数说明:
- 参数一:
id object
: 给哪个对象添加属性。 - 参数二:
const void *key
: 关联对象中属性值存取过程中对应唯一标识, 根据key来设置和取值。 - 参数三:
id value
: 关联的值,也就是set方法传入的值给属性去保存。 - 参数四:
objc AssociationPolicy policy
: 策略,属性以什么形式保存。
typedef OBJC ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ ASSOCIATION_ASSIGN = 0, //指定个弱引用相关联的对象
OBJC_ ASSOCIATION_RETAIN_NONATOMIC = 1; //指定相关对象的强引用, 非原子性
OBJC_ ASSOCIATION_COPY_NONATOMIC = 3; //指定相关的对象被复制, 非原子性
OBJC_ ASSOCIATION_RETAIN = 01401; //指定相关对象的强引用,原子性
OBJC_ ASSOCIATION_COPY = 01403; //指定相关的对象被复制, 原子性
};
关联对象原理
实现关联对象的核心对象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
- 其中Map就相当于我们平时使用的字典,也是key-value 存取值。
通过一张图整理其中的关系:
- 关联对象并不是存储在关联对象本身内存中
- 关联对象存储在全局的一个
AssociationsManager
中 - 设置关联对象为
nil
,就相当于移除了关联对象
实例
下面举个例子,定义好一个Person类,然后在其分类Person+Per中添加属性
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Per)
@property (nonatomic, strong) NSString* name;
@property (nonatomic, assign) int name;
@end
NS_ASSUME_NONNULL_END
#import "Person+Per.h"
#import <objc/runtime.h>
@implementation Person (Per)
给key设值有三种比较好的的方法
因为key值类型为const void*,任何类型的值,其实就是给个唯一的标识
1.我们针对每个属性,定义-个全局的key名, 然后取其地址,这一定是唯一的加上static,只在文件内部有效
static const void *NameKey = &NameKey;
static const void *WeightKey = &WeightKey;
2.针对每个属性,因为类中的属性名是唯一的,直接拿属性名作为key
#define NameKey = @"name";
#define WeightKey = @"weight";
3.使用@selector作为key
直接用属性名对应的get方法的selector,有提示不容易写错
并且get方法隐藏参数cmd 可以直接用,看上去就会更加简洁
以下实例代码就是用的第三种方式
- (void)setName:(NSString *)name {
//通过一个key给对象关联一个值
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
//通过一个key,取出对象所关联的值
//隐式参数 _cmd = @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
- (void)setWeight:(int)weight {
objc_setAssociatedObject(self, @selector(weight, @(weight), OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (int)weight {
//通过一个key,取出对象所关联的值
//隐式参数 _cmd = @selector(weight)
return objc_getAssociatedObject(self, _cmd);
}
@end
设置好关联对象后就可以在外部正常调用添加的属性了
什么是_cmd?
在上面使用@selector
作为key(即第三种方法)进行关联对象时,涉及到到了_cmd
参数
_cmd
是隐藏的参数,表示当前方法的selector
,他和self表示当前方法调用的对象实例。
- 获取当前被调用方法:
NSStringFromSelector(_cmd)
- 在运行时时使用:在某个分类方法里为对象动态添加属性,由于
_cmd
是在编译时候(compile - time)就可以确定的值,因此可以直接使用