前言

OC的关联对象经常被用来存储Category中的属性对应的实例变量,当然它还有其他的用途。

这篇文章将讨论关联对象是如何实现的,以及关联对象的内存管理问题。

Demo

先用一段代码来引入要讲的东西:

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
#import "HCBase.h"

@interface HCBase (vanney)

@property (nonatomic, assign) int cui;
@property (nonatomic, strong) NSString *van;

@end

/* ----------------- 这里是分割线 -------------- */

#import "HCBase+vanney.h"

@implementation HCBase (vanney)

- (void)setCui:(int)cui {
objc_setAssociatedObject(self, "cui", @(cui), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)cui {
return [objc_getAssociatedObject(self, "cui") intValue];
}

- (void)setVan:(NSString *)van {
objc_setAssociatedObject(self, "van", van, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)van {
return objc_getAssociatedObject(self, "van");
}

@end

可以看到:

  1. 在头文件中,我们定义了两个属性 cuivan
  2. 在.m文件中,我们使用了关联对象来存储这两个实例变量
  3. 使用关联对象时,我们还指定了它的内存管理的方式 OBJC_ASSOCIATION_RETAIN_NONATOMICOBJC_ASSOCIATION_COPY_NONATOMIC

关联对象的实现

关联对象的实现是开源的,在runtime的objc-references.m源文件中。在OC中,使用一个全局的表来存储所有的关联对象,如下图所示:

association

关于这张图的解释可以参考我的这篇文章Objective-C runtime - 分类与关联对象

上面的Demo使用了下面两个函数来存取关联对象,我们来看看他们的源码

  1. objc_setAssociatedObject
  2. objc_getAssociatedObject

objc_setAssociatedObject

这个方法用来设置关联对象:

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
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// 关联对象使用 ObjcAssociation 这个c++对象来存储。
// 设置新对象,那么有可能有老的对象会被废弃。那么新建一个ObjcAssociation对象,之后用来存储老的对象
ObjcAssociation old_association(0, nil);

// acquireValue函数: 设置新的对象的retainCount,也就是对他进行手动内存管理
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);

// 如果确实是要设置新关联对象的时候
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);

// 这个对象之前设置过关联对象,也就是说该对象的关联对象数量大于1
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);

// 寻找全局的关联对象表,如果存在老对象的话
if (j != refs->end()) {
// 存储老对象到之前新建的ObjcAssociation对象中
old_association = j->second;
// 设置新的关联对象
j->second = ObjcAssociation(policy, new_value);
}
// 不存在老对象,直接设置新关联对象
else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
}

// 这个对象之前没有设置过任何的关联对象
else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);

// 将对象的isa的has_assoc字段设置成true
object->setHasAssociatedObjects();
}
}
// 要将关联对象设成 nil,也就是取消该关联对象。先存老对象,再在全局表中删除该关联对象
else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}


// 释放老对象,retainCount相关,内存管理相关
if (old_association.hasValue()) ReleaseValue()(old_association);
}

可以看到设置新的关联对象其实很简单:

  1. 现在全局关联对象表中寻找这个key
  2. 先存原来关联对象
  3. 再设置新的关联对象
  4. 释放原有的关联对象

objc_getAssociatedObject

获取关联对象就更简单了,直接到表中去找对应的key的对象就好了。

关联对象的内存管理

在上面讲到的源代码中,涉及到管理对象内存管理的有两个函数

  1. acquireValue
  2. ReleaseValue

requireValue

1
2
3
4
5
6
7
8
9
static id acquireValue(id value, uintptr_t policy) {
switch (policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
case OBJC_ASSOCIATION_SETTER_COPY:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
}
return value;
}

可以看到,设置关联对象的内存管理方式和你手动传进来的policy有关。

将policy和0XFF相与:

  1. 结果是OBJC_ASSOCIATION_SETTER_RETAIN的话,就调用对象的retain函数,也就是将retainCount加一
  2. 结果是OBJC_ASSOCIATION_SETTER_COPY的话,就调用对象的copy函数,也就是将拷贝对象,并给新对象的retainCount赋1

PS :

  1. 没有找到具体的SEL_copy的实现。
  2. 至于retain的实现,其实就是将对象的isa的extra_rc字段加1 具体可以参考这篇博客OC源码 —— retain和release

各种常量

在我们自己调用存取关联对象的时候使用了objc_AssociationPolicy常量:

1
2
3
4
5
6
7
8
9
10
11
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};

根据名字很容易想到所对应的内存管理语义。

objc-references.mm中使用的是另外的常量:

1
2
3
4
5
6
7
8
enum { 
OBJC_ASSOCIATION_SETTER_ASSIGN = 0,
OBJC_ASSOCIATION_SETTER_RETAIN = 1,
OBJC_ASSOCIATION_SETTER_COPY = 3, // NOTE: both bits are set, so we can simply test 1 bit in releaseValue below.
OBJC_ASSOCIATION_GETTER_READ = (0 << 8),
OBJC_ASSOCIATION_GETTER_RETAIN = (1 << 8),
OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8)
};

可以看到这两个enum中,前三个是相对应的,他们有相同的值

ReleaseValue

1
2
3
4
5
6
7
8
9
10
11
static void releaseValue(id value, uintptr_t policy) {
if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_release);
}
}

struct ReleaseValue {
void operator() (ObjcAssociation &association) {
releaseValue(association.value(), association.policy());
}
};

再来看看如何释放对象。可以看到,他是将关联对象的policyOBJC_ASSOCIATION_SETTER_RETAIN相与,简单计算就会发现:当policy为retain和copy时,这个if判断都是正确的,也就会对相应关联对象执行release操作。和我们预想的是一致的。

参考