iOS开发-关于Associated Objects

一、前言

Associated Objects(关联对象)是什么?什么时候用?为什么要用?怎么用?
最开始用到关联对象是源于一个需求(废话,肯定是源于需求)。
大家都知道,Button的点击事件,一定是将本身传入参数:

1
2
3
4
5
6
7
8
9
10
- (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对象,但此时,我也很无奈。。。就先借用一下吧,哈哈
    也就是这样👇👇👇
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - (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

    1
    #import <objc/runtime.h>
  • API主要就是(来自系统文件runtime.h的介绍)👇👇👇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    /** 
    * 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 来移除某个已有的关联对象。如下这样👇👇👇

1
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了

三、用关联对象解决上述问题

  • 传整型数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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);
    }
  • 传对象数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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+

具体代码如下:👇👇👇

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>

@interface UIBarButtonItem (Badge)

@property (assign, nonatomic) UIColor *badgeColor;

- (void)configBadgeWithBigNum:(NSInteger)bigNum small:(BOOL)isOn;

@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#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 。
    Associated.jpg