一、前言
Associated Objects(关联对象)是什么?什么时候用?为什么要用?怎么用?
最开始用到关联对象是源于一个需求(废话,肯定是源于需求)。
大家都知道,Button的点击事件,一定是将本身传入参数:
- (void)setupFoundationUI {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.frame = (CGRect){0, 0, 30, 30};
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnDidClick:(UIButton *)sender {
NSLog(@"Btn did click ...");
}
如果想要传入一个特定的参数呢?
- 当时我想传的参数是整型,于是我想到了tag(那时的我还不知道关联对象)
- tag其实是用来标记不同的Button对象,但此时,我也很无奈。。。就先借用一下吧,哈哈
也就是这样👇👇👇
- (void)setupFoundationUI {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.frame = (CGRect){0, 0, 30, 30};
btn.tag = 110;
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnDidClick:(UIButton *)sender {
NSLog(@"Btn did click ...tag = %zd", sender.tag);
}
后来又有需求,要传的参数是字符串,甚至是对象。。。
就在此时我注意到了关联对象(Associated Objects)
二、关联对象的介绍
1.关联对象解决的问题
我们知道,在 Objective-C 中可以通过 Category (类别、分类,反正你们懂得)给一个现有的类添加属性,但是却不能添加实例变量,这似乎成为了 Objective-C 的一个明显短板,关联对象就可以解决这个问题。
2.如何用关联对象
- 首先要引入 runtime
#import <objc/runtime.h>
- API主要就是(来自系统文件runtime.h的介绍)👇👇👇
/**
* Sets an associated value for a given object using a given key and association policy.
*
* @param object The source object for the association.
* @param key The key for the association.
* @param value The value to associate with the key key for object. Pass nil to clear an existing association.
* @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
/**
* Returns the value associated with a given object for a given key.
*
* @param object The source object for the association.
* @param key The key for the association.
*
* @return The value associated with the key \e key for \e object.
*
* @see objc_setAssociatedObject
*/
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
/**
* Removes all associations for a given object.
*
* @param object An object that maintains associated objects.
*
* @note The main purpose of this function is to make it easy to return an object
* to a "pristine state”. You should not use this function for general removal of
* associations from objects, since it also removes associations that other clients
* may have added to the object. Typically you should use \c objc_setAssociatedObject
* with a nil value to clear an association.
*
* @see objc_setAssociatedObject
* @see objc_getAssociatedObject
*/
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
官方的解释已经很清晰了,就不过多解读了(绑定、获取、移除),值得注意的一点是:objc_removeAssociatedObjects
是移除一个对象的所有关联对象,将该对象恢复成“原始”状态,这样的操作风险太大,所以一般的做法是通过给 objc_setAssociatedObject
函数传入 nil 来移除某个已有的关联对象。如下这样👇👇👇
objc_setAssociatedObject(self, &key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- 关于key的问题
- 声明 static char kAssociatedObjectKey; 使用 &kAssociatedObjectKey 作为 key 值;
- 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; 使用 kAssociatedObjectKey 作为 key 值;
- 用 selector ,使用 getter 方法的名称作为 key 值。
- 关于policy(关联策略)的问题
OBJC_ASSOCIATION_ASSIGN
等价属性@property (assign) or @property (unsafe_unretained)
弱引用关联对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC
等价属性@property (strong, nonatomic)
强引用关联对象,且为非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC
等价属性@property (copy, nonatomic)
复制关联对象,且为非原子操作
OBJC_ASSOCIATION_RETAIN
等价属性@property (strong, atomic)
强引用关联对象,且为原子操作
OBJC_ASSOCIATION_COPY
等价属性@property (copy, atomic)
复制关联对象,且为原子操作
具体内容可以参考官方文档,这里就不copy了
三、用关联对象解决上述问题
- 传整型数据
NSString *const kButtonKey = @"kButtonKey";
- (void)setupFoundationUI {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
objc_setAssociatedObject(btn, &kButtonKey, @110, OBJC_ASSOCIATION_ASSIGN);
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnDidClick:(UIButton *)sender {
NSInteger value = [objc_getAssociatedObject(sender, &kButtonKey) integerValue];
NSLog(@"btn did click ...value = %zd", value);
}
- 传对象数据
NSString *const kButtonKey = @"kButtonKey";
- (void)setupFoundationUI {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.frame = (CGRect){0, 0, 30, 30};
Person *person = [[Person alloc] init];
person.name = @"LiMing";
objc_setAssociatedObject(btn, &kButtonKey, person, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnDidClick:(UIButton *)sender {
Person *person = objc_getAssociatedObject(sender, &kButtonKey);
NSLog(@"person`s name = %@", person.name);
}
四、关联对象用于Category
以实现UIBarButtonItem的扩展为例子,为其增加红点的功能,其中大量的使用了关联对象
需求:
1.显示小红点
2.显示数字红点
3.即有小红点又有数字红点时,优先显示数字红点
4.可自定义红点颜色(默认是红色[UIColor redColor])
5.数字红点数目大于99时,显示99+
具体代码如下:👇👇👇
#import <UIKit/UIKit.h>
@interface UIBarButtonItem (Badge)
@property (assign, nonatomic) UIColor *badgeColor;
- (void)configBadgeWithBigNum:(NSInteger)bigNum small:(BOOL)isOn;
@end
#import "UIBarButtonItem+Badge.h"
#import <objc/runtime.h>
NSString *const ZYBarButtonItem_hasBadgeKey = @"ZYBarButtonItem_hasBadgeKey";
NSString *const ZYBarButtonItem_badgeKey = @"ZYBarButtonItem_badgeKey";
NSString *const ZYBarButtonItem_badgeSizeKey = @"ZYBarButtonItem_badgeSizeKey";
NSString *const ZYBarButtonItem_badgeOriginXKey = @"ZYBarButtonItem_badgeOriginXKey";
NSString *const ZYBarButtonItem_badgeOriginYKey = @"ZYBarButtonItem_badgeOriginYKey";
NSString *const ZYBarButtonItem_badgeColorKey = @"ZYBarButtonItem_badgeColorKey";
NSString *const ZYBarButtonItem_badgeSizeWKey = @"ZYBarButtonItem_badgeSizeWKey";
@interface UIBarButtonItem ()
@property (nonatomic, assign) CGFloat badgeSizeW;
@property (strong, nonatomic) UILabel *badge;
@property (assign, nonatomic) CGFloat badgeOriginX;
@property (assign, nonatomic) CGFloat badgeOriginY;
@property (assign, nonatomic) CGFloat badgeSize;
@property BOOL hasBadge;
@end
@implementation UIBarButtonItem (Badge)
- (void)initBadge {
UIView *superview = nil;
if (self.customView) {
superview = self.customView;
superview.clipsToBounds = NO;
} else if ([self respondsToSelector:@selector(view)] && [(id)self view]) {
superview = [(id)self view];
}
[superview addSubview:self.badge];
// 默认设置 default configure
self.badgeColor = [UIColor redColor];
self.badgeSize = 10;
self.badgeSizeW = 10;
self.badgeOriginX = 28;
self.badgeOriginY = 8;
self.badge.hidden = YES;
self.badge.layer.masksToBounds = YES;
self.badge.font = [UIFont boldSystemFontOfSize:12];
self.badge.textAlignment = NSTextAlignmentCenter;
self.badge.textColor = [UIColor whiteColor];
}
- (void)showBadge {
self.badge.hidden = NO;
}
- (void)hideBadge {
self.badge.hidden = YES;
}
- (void)refreshBadge {
self.badge.frame = (CGRect){self.badgeOriginX,self.badgeOriginY,self.badgeSizeW,self.badgeSize};
self.badge.backgroundColor = self.badgeColor;
self.badge.layer.cornerRadius = self.badgeSize/2;
}
#pragma mark ---------- badge getter & setter function -----------
- (UILabel *)badge {
UILabel *badge = (UILabel *)objc_getAssociatedObject(self, &ZYBarButtonItem_badgeKey);
if (!badge) {
badge = [[UILabel alloc] init];
[self setBadge:badge];
[self initBadge];
}
return badge;
}
- (void)setBadge:(UILabel *)badge {
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeKey, badge, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIColor *)badgeColor {
return objc_getAssociatedObject(self, &ZYBarButtonItem_badgeColorKey);
}
- (void)setBadgeColor:(UIColor *)badgeColor {
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeColorKey, badgeColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}
-(CGFloat)badgeSize {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeSizeKey);
return number.floatValue;
}
-(void)setBadgeSize:(CGFloat)badgeSize {
NSNumber *number = [NSNumber numberWithDouble:badgeSize];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeSizeKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}
- (CGFloat)badgeSizeW {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeSizeWKey);
return number.floatValue;
}
- (void)setBadgeSizeW:(CGFloat)badgeSizeW {
NSNumber *number = [NSNumber numberWithDouble:badgeSizeW];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeSizeWKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}
-(CGFloat)badgeOriginX {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeOriginXKey);
return number.floatValue;
}
-(void)setBadgeOriginX:(CGFloat)badgeOriginX {
NSNumber *number = [NSNumber numberWithDouble:badgeOriginX];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeOriginXKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}
-(CGFloat)badgeOriginY {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeOriginYKey);
return number.floatValue;
}
-(void)setBadgeOriginY:(CGFloat)badgeOriginY {
NSNumber *number = [NSNumber numberWithDouble:badgeOriginY];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeOriginYKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}
- (void)setHasBadge:(BOOL)hasBadge {
if (hasBadge) {
[self showBadge];
}else{
[self hideBadge];
}
NSNumber *number = [NSNumber numberWithBool:hasBadge];
objc_setAssociatedObject(self, &ZYBarButtonItem_hasBadgeKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)hasBadge {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_hasBadgeKey);
return number.boolValue;
}
#pragma mark - Public
- (void)configBadgeWithBigNum:(NSInteger)bigNum small:(BOOL)isOn {
if (bigNum > 0) {
self.hasBadge = YES;
self.badgeSize = 18;
self.badgeOriginY = 6;
NSString *numStr = [NSString stringWithFormat:@"%zd", bigNum];
if (bigNum < 10) {
self.badgeSizeW = 18;
} else if (bigNum < 100) {
self.badgeSizeW = 25;
} else {
self.badgeSizeW = 30;
numStr = @"99+";
}
self.badge.text = numStr;
} else if (isOn) {
self.hasBadge = YES;
self.badgeSizeW = 10;
self.badgeSize = 10;
self.badgeOriginY = 8;
self.badge.text = nil;
} else {
self.hasBadge = NO;
}
}
@end
五、写在最后
- 关联对象与被关联对象本身的存储并没有直接的关系,它是存储在单独的哈希表中的;
- 关联对象的五种关联策略与属性的限定符非常类似,在绝大多数情况下,我们都会使用
OBJC_ASSOCIATION_RETAIN_NONATOMIC
的关联策略,这可以保证我们持有关联对象; - 关联对象的释放时机与移除时机并不总是一致,比如用关联策略
OBJC_ASSOCIATION_ASSIGN
进行关联的对象,很早就已经被释放了,但是并没有被移除,而再使用这个关联对象时就会造成 Crash 。