我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用@property 并不能在分类中正确创建实例变量和存取方法。
不过,通过 Objective-C 运行时中的关联对象,也就是 Associated Object,我们可以实现上述需求。
分类中的 @property :@property 可以说是一个 Objective-C 编程中的“宏”,它有元编程的思想。
@interface Object : NSObject
@property (nonatomic, strong) NSString *property;
@end
在使用上述代码时会做三件事:
1.生成实例变量 _property
2.生成 getter 方法 - property
3.生成 setter 方法 - setProperty:
@implementation Object{
NSString *_property;
}
- (NSString *)property {
return _property;
}
- (void)setProperty:(NSString *)property {
_property = property;
}
@end
这些代码都是编译器为我们生成的,虽然你看不到它,但是它确实在这里,我们既然可以在类中使用。但分类中的 @property 并没有为我们生成实例变量以及存取方法,而需要我们手动实现。
使用关联对象
Q:我们为什么要使用关联对象?
A:因为在分类中 @property 并不会自动生成实例变量以及存取方法,所以一般使用关联对象为已经存在的类添加"属性"。
下面,会介绍 ObjC 运行时为我们提供的与关联对象有关的 API,并在分类中实现一个伪属性:
NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self, _cmd);
}
注:这里的 _cmd 代指当前方法的选择子,也就是 @selector(categoryProperty)。
我们使用了两个方法 objc_getAssociatedObject 以及 objc_setAssociatedObject 来模拟“属性"的存取方法,而使用关联对象模拟实例变量。
在这里需要解释下我们为什么会向方法中传入 @selector(categoryProperty)?
关于上面这个问题,我们须得看下方法的原型:
id objc_getAssociatedObject(id object, const void *key);
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
从上面的方法原型中我们可以看出@selector(categoryProperty) 也就是参数中的 key,其实可以使用静态指针 static void * 类型的参数来代替,不过在这里,还是强烈推荐使用 @selector(categoryProperty) 作为 key 传入。因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性。(当然也避免了起名不会起的原因)
下来需要解释的一个就是OBJC_ASSOCIATION_RETAIN_NONATOMIC
而我们在代码中实现的属性 associatedObject 就相当于使用了 nonatomic 和 strong 修饰符。
说到上面2个方法一个设置和一个获取,那么哪有一个删除objc_removeAssociatedObjects
你可能会在刚开始接触对象关联时想要尝试去调用objc_removeAssociatedObjects() 来进行删除操作,但如文档中所述,你不应该自己手动调用这个函数。此函数的主要目的是在“初试状态”时方便地返回一个对象。你不应该用这个函数来删除对象的属性,因为可能会导致其他客户对其添加的属性也被移除了。规范的方法是:调用objc_setAssociatedObject方法并传入一个nil 值来清除一个关联。
应用:
添加私有属性用于更好地去实现细节。当扩展一个内建类的行为时,保持附加属性的状态可能非常必要。
添加public属性来增强category的功能。有些情况下这种(通过关联对象)让category行为更灵活的做法比在用一个带变量的方法来实现更有意义。
创建一个用于KVO的关联观察者。当在一个category的实现中使用KVO时,建议用一个自定义的关联对象而不是该对象本身作观察者。