什么是关联对象
关联对象是指某个对象通过一个唯一的key连接到一个类的实例上。我们都知道,可以使用Category来扩展方法,那Category可以添加属性吗?相信很多人都会回答不可以,答案其实是可以的,只是不会自动生成getter/setter方法的实现,也不会自动生成成员变量,在我们调用的时候就会crash了。如果要添加一个属性,那就要用到runtime的关联对象了。
如何关联对象
来看下runtime提供给我们的3个API方法:1
2
3
4
5
6// 关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
// 移除关联的对象
void objc_removeAssociatedObjects(id object)
相关说明:
- id object:被关联的对象
- const void *key:关联的key,要求唯一
- id value:关联的对象
- objc_AssociationPolicy policy:内存管理的策略
objc_AssociationPolicy的枚举值和相关说明:1
2
3
4
5
6
7typedef 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 // 指定相关的对象被复制,原子性
};
当对象被释放时,会根据这个策略会决定是否释放关联的对象,当策略是RETAIN/COPY时,会释放关联的对象,当是ASSIGN时,将不会释放。需要注意的是,我们不需要主动调用removeAssociated方法来解除关联的对象,如果需要解除指定的对象,可以使用setAssociatedObject置nil来实现。
使用步骤:
- 创建对应类的一个Category,创建要添加的属性。
- 重写属性的setter方法,在setter方法中调用objc_setAssociatedObject进行关联对象。
- 重写属性的getter方法,在getter方法中return objc_getAssociatedObject(获取到的关联对象)
- 需要注意的是:key一定要唯一,可以定义成宏。
应用场景
- 给UITapGestureRecognizer动态添加一个NSString类型的属性,方便传递字符串类型的信息,当然UITapGestureRecognizer本身也有tag。
在.h文件中进行声明1
2
3
4
5
6
7
8
9NS_ASSUME_NONNULL_BEGIN
@interface UITapGestureRecognizer (NSString)
@property (nonatomic, strong) NSString *tapString;
@end
NS_ASSUME_NONNULL_END
在.m文件中1
2
3
4
5
6
7
8
9
10
11static NSString *tapStringKey = @"tapString";
@implementation UITapGestureRecognizer (NSString)
- (void)setTapString:(NSString *)tapString {
objc_setAssociatedObject(self, &tapStringKey, tapString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tapString {
return objc_getAssociatedObject(self, &tapStringKey);
}
在viewController中就可以调用了:1
2
3
4
5
6
7UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
tap.tapString = @"触摸了!";
[self.view addGestureRecognizer:tap];
- (void)tapAction:(UITapGestureRecognizer *)tap {
NSLog(@"%@", tap.tapString);
}
- 假如封装好了一个页面缺省图,我们定义为BlankPageView,如果我们要为每个UIView添加一个blankPageView属性时,可以使用关联对象的方法。
在setter方法中:1
2
3
4
5
6
7static char BlankPageViewKey;
- (void)setBlankPageView:(BlankPageView *)blankPageView {
objc_setAssociatedObject(self, &BlankPageViewKey,
blankPageView,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
在getter方法中:1
2
3- (BlankPageView *)blankPageView {
return objc_getAssociatedObject(self, &BlankPageViewKey);
}
- 以UIButton为例,使用关联对象完成一个函数的点击回调
在分类的.h文件中1
2
3
4
5
6
7typedef void(^BtnActionCallBack)(UIButton *btn);
@interface UIButton (ActionBlock)
- (void)btnActionCallBack:(BtnActionCallBack)callBack;
@end
在.m文件中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15static NSString *btnActionKey = @"btnAction";
- (void)btnActionCallBack:(BtnActionCallBack)callBack {
objc_setAssociatedObject(self, &btnActionKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnAction {
BtnActionCallBack callBack = objc_getAssociatedObject(self, &btnActionKey);
if (callBack) {
callBack(self);
}
}
使用:1
2
3[self.testBtn btnActionCallBack:^(UIButton * _Nonnull btn) {
NSLog(@"button action call back");
}];
- 关联观察者对象
当在一个Category的实现中使用KVO时,建议用一个自定义的关联对象而不是该对象本身作为观察者。比如AFNetworking,为loading控件监听NSURLSessionTask以获取网络进度的分类中:1
2
3
4
5
6
7
8
9
10
11
12
13
14@implementation UIActivityIndicatorView (AFNetworking)
- (AFActivityIndicatorViewNotificationObserver *)af_notificationObserver {
AFActivityIndicatorViewNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver));
if (notificationObserver == nil) {
notificationObserver = [[AFActivityIndicatorViewNotificationObserver alloc] initWithActivityIndicatorView:self];
objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return notificationObserver;
}
- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
[[self af_notificationObserver] setAnimatingWithStateOfTask:task];
}
@end