详解iOS中的关联对象

首发于我的个人博客

从给分类添加属性说起

详解iOS中分类Cateogry 一文中,我们提出一个问题,

Category能否添加成员变量?如果可以,如何给Category添加成员变量?

  • 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果,用关联对象技术

那这里就详细说明

添加属性,实际上都做了什么

首先我们要回忆一下,添加属性,实际上做了三件事

  • 生成成员变量
  • 生成set方法和get方法的声明
  • 生成set方法和get方法的实现

eg:
定义一个 YZPerson 类,并定义age属性

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

@interface YZPerson : NSObject

@property (assign, nonatomic) int age;


@end

就相当于干了三件事

  • 生成成员变量_age
  • 生成set方法和get方法的声明
  • 生成set方法和get方法的实现
    如下
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
#import <Foundation/Foundation.h>


@interface YZPerson : NSObject

{
int _age;
}
- (void)setAge:(int)age;
- (int)age;

@end



#import "YZPerson.h"

@implementation YZPerson
- (void)setAge:(int)age{
_age = age;
}

- (int)age{
return _age;
}
@end

那在分类中添加属性怎么就不行?

先说结论

  • 生成成员变量_age
  • 会生成set方法和get方法的声明
  • 不会生成set方法和get方法的实现

不会生成set方法和get方法的实现

定义一个分类 YZPerson+Ext.h,然后添加属性weight

1
2
3
4
#import "YZPerson.h"
@interface YZPerson (Ext)
@property (nonatomic ,assign) int weight;
@end

使用

1
2
YZPerson *person = [[YZPerson alloc] init];
person.weight = 10;

会直接报错,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
iOS-关联对象[1009:10944] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff3550d063 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff6ac8e06b objc_exception_throw + 48
2 CoreFoundation 0x00007fff355961bd -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff354b34b4 ___forwarding___ + 1427
4 CoreFoundation 0x00007fff354b2e98 _CF_forwarding_prep_0 + 120

6 libdyld.dylib 0x00007fff6c0183f9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Program ended with exit code: 9

reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10' 可知,分类中添加属性,没有生成set方法和get方法的实现

会生成set方法和get方法的声明

1
2
3
4
5
6
7
8
9
10
#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)
- (void)setWeight:(int)weight{

}
- (int)weight{
return 100;
}
@end

然后再调用

1
2
3
4
5
YZPerson *person = [[YZPerson alloc] init];
person.age = 25;
person.weight = 10;
NSLog(@"person.age = %d",person.age);
NSLog(@"person.weight = %d",person.weight);

输出

1
2
2019-07-10 08:28:04.406972+0800 iOS-关联对象[1620:18520] person.age = 25
2019-07-10 08:28:04.407291+0800 iOS-关联对象[1620:18520] person.weight = 100

进一步证明了,不会生成set方法和get方法的实现,但是会生成set方法和get方法的声明,因为如果没有生成set方法和get方法的声明,这个方法就不能调用。

我们还可以这样:在YZPerson+Ext.h文件中声明了weight,然后再YZPerson+Ext.m中写实现的时候,会有提示的

更加说明了是有声明的。

分类中不能直接定义成员变量

1
2
3
4
5
6
7
8
9
#import "YZPerson.h"


@interface YZPerson (Ext)
{
int _weight; // 报错 Instance variables may not be placed in categories
}
@property (nonatomic ,assign) int weight;
@end

会直接报错Instance variables may not be placed in categories,成员变量不能定义在分类中

源码角度证明

前面的文章详解iOS中分类Cateogry 中分析过源码,objc-runtime-new.h中分类结构体是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

可知,这个结构体中,没有数组存放成员变量,只有属性,协议等。

怎么来完善属性

有什么办法可以实现在分类中添加属性和在类中添加属性一样的效果么?答案是有的

方案一 用全局变量

分类YZPerson+Ext.m中定义全局变量 _weight

1
2
3
4
5
6
7
8
9
10
11
12
13
14

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)

int _weight;

- (void)setWeight:(int)weight{
_weight = weight;
}
- (int)weight{
return _weight;
}
@end

使用时候

1
2
3
YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);

输出为

1
iOS-关联对象[1983:23793] person.weight = 103

看起来确实可以,然后实际上我们不能这么用,因为,全局变量是共享的,假设有两个 Person,第二个Person修改了weight属性,然后打印第一个Person.weight

1
2
3
4
5
6
7
YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);

YZPerson *person2 = [[YZPerson alloc] init];
person2.weight = 10;
NSLog(@"person.weight = %d",person.weight);

输出为

1
2
iOS-关联对象[1983:23793] person.weight = 103
iOS-关联对象[1983:23793] person.weight = 10

可知,修改了Person2.weight 会改变Person.weight的值,因为是全局变量的缘故。所以这种方法不行

方案二 用字典

既然前面方案不能用的原因是全局变量,共享一份,那我们是不是只要保证,一对一的关系,是不是就可以了呢?

定义 字典weights_ 以对象的地址值作为key来,weight的值作为value来存储和使用

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

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)

NSMutableDictionary *weights_;

+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 写在这里,保证s只初始化一次
weights_ = [NSMutableDictionary dictionary];
});
}

- (void)setWeight:(int)weight{
NSString *key = [NSString stringWithFormat:@"%p",self];//self 地址值作为key
weights_[key] = @(weight);//字典中的value不能直接放int,需要包装成对象
}
- (int)weight{
NSString *key = [NSString stringWithFormat:@"%p",self];
return [weights_[key] intValue];
}

@end

这样的话,使用起来,就不会因为不同对象而干扰了
结果如下

存在的问题

  • 因为是全局的,存在内存泄露问题
  • 线程安全问题,多个线程同时访问的话,有线程安全问题
  • 代码太多,如果每次增加一个属性,都要写好多代码。不利于维护

关联对象方案

关联对象的使用

下面先简单说明关联对象的使用

动态添加

1
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
  • 参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self
  • 参数二:void * == id key : key值,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
  • 参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
  • 参数四:objc_AssociationPolicy policy: 策略,属性以什么形式保存。
1
2
3
4
5
6
7
typedef 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 // 指定相关的对象被复制,原子性
};

整理成表格如下

objc_AssociationPolicy 对应的修饰符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

eg: 我们在代码中使用了 OBJC_ASSOCIATION_RETAIN_NONATOMIC 就相当于使用了 nonatomicstrong 修饰符。

注意点
上面列表中,没有对应weak修饰的策略,
原因是
object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object

1
disguised_ptr_t disguised_object = DISGUISE(object);

weak修饰的属性,当没有拥有对象之后就会被销毁,并且指针置为nil,那么在对象销毁之后,虽然在map中仍然存在值object对应的AssociationsHashMap,但是因为object地址已经被置为nil,会造成坏地址访问而无法根据object对象的地址转化为disguised_object了,这段话可以再看完全文之后,再回来体会下。

取值

1
objc_getAssociatedObject(id object, const void *key);
  • 参数一:id object : 获取哪个对象里面的关联的属性。
  • 参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value

移除关联对象

1
2
3
4
5
- (void)removeAssociatedObjects
{
// 移除关联对象
objc_removeAssociatedObjects(self);
}

具体应用

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

#import "YZPerson.h"

@interface YZPerson (Ext)
@property (nonatomic,strong) NSString *name;
@end


#import "YZPerson+Ext.h"
#import <objc/runtime.h>
@implementation YZPerson (Ext)

const void *YZNameKey = &YZNameKey;

- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, YZNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
return objc_getAssociatedObject(self, YZNameKey);
}

- (void)dealloc
{
objc_removeAssociatedObjects(self);
}

@end

使用的时候,正常使用,就可以了

1
2
3
4
5
6
7
8
YZPerson *person = [[YZPerson alloc] init];
person.name = @"jack";

YZPerson *person2 = [[YZPerson alloc] init];
person2.name = @"rose";

NSLog(@"person.name = %@",person.name);
NSLog(@"person2.name = %@",person2.name);

输出

1
2
iOS-关联对象[4266:52285] person.name = jack
iOS-关联对象[4266:52285] person2.name = rose

使用起来就是这么简单

关联对象原理

四个核心对象

实现关联对象技术的核心对象有

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

源码解读

关联对象的源码在 Runtime源码

objc_setAssociatedObject

查看objc-runtime.mm类,首先找到objc_setAssociatedObject函数,看一下其实现

1
2
3
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}

_object_set_associative_reference

查看

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

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
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);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
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);
object->setHasAssociatedObjects();
}
} 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);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}

如图所示

_object_set_associative_reference函数内部我们可以找到我们上面说过的实现关联对象技术的四个核心对象。接下来我们来一个一个看其内部实现原理探寻他们之间的关系。

AssociationsManager

查看 AssociationsManager 我们知道AssociationsManager 内部有static AssociationsHashMap *_map;

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }

AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};

AssociationsHashMap

接下来看 AssociationsHashMap

上图中 AssociationsHashMap的源码我们发现AssociationsHashMap继承自unordered_map首先来看一下unordered_map内的源码

unordered_map源码中我们可以看出 参数 _Key_Tp 对应着map中的KeyValue,那么对照上面AssociationsHashMap的源码,可以发现_Key中传入的是unordered_map<disguised_ptr_t_Tp中传入的值则为ObjectAssociationMap *

然后 我们查看ObjectAssociationMap的源码,上图中ObjectAssociationMap已经标记出,我们可以知道ObjectAssociationMap中同样以keyValue的方式存储着ObjcAssociation

ObjcAssociation

接着我们来到ObjcAssociation中,可以看到

1
2
3
4
5
6
7
8
9
10
11
12
class ObjcAssociation {
uintptr_t _policy; // 策略
id _value; // value值
public:
ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
ObjcAssociation() : _policy(0), _value(nil) {}

uintptr_t policy() const { return _policy; }
id value() const { return _value; }

bool hasValue() { return _value != nil; }
};

从上面的代码中,我们发现ObjcAssociation存储着_policy_value,而这两个值我们可以发现正是我们调用objc_setAssociatedObject函数传入的值,换句话说我们在调用objc_setAssociatedObject函数中传入valuepolicy这两个值最终是存储在ObjcAssociation中的。

现在我们已经对四个核心对象AssociationsManagerAssociationsHashMapObjectAssociationMapObjcAssociation之间的关系有了初步的了解,那么接下继续仔细阅读源码,看一下objc_setAssociatedObject函数中传入的四个参数分别放在哪个对象中充当什么作用

细读 _object_set_associative_reference

_object_set_associative_reference的代码中

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


void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
// 根据value的值通过acquireValue函数获取得到new_value
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
// 获取 manager 内的 AssociationsHashMap 也就是 associations
AssociationsHashMap &associations(manager.associations());
// object 经过 DISGUISE 函数被转化为了disguised_ptr_t类型的disguised_object
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
// policy和new_value 作为键值对存入了ObjcAssociation
j->second = ObjcAssociation(policy, new_value);
} else {
// policy和new_value 作为键值对存入了ObjcAssociation
(*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);
object->setHasAssociatedObjects();
}
} else {
// 来到这里说明,value为空
// 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);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}

acquireValue内部实现 通过对策略的判断返回不同的值

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 objc_retain(value);
case OBJC_ASSOCIATION_SETTER_COPY:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
}
return value;
}
  • 首先根据我们传入的value经过acquireValue函数处理返回了new_valueacquireValue函数内部其实是通过对策略的判断返回不同的值
1
2
3
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
  • 之后创建AssociationsManager manager,得到manager内部的AssociationsHashMapassociations
    之后我们看到了我们传入的第一个参数object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object
1
2
3
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
  • 之后被处理成new_valuevalue,和policy一起被存入了ObjcAssociation中。
    ObjcAssociation对应我们传入的key被存入了ObjectAssociationMap中。
    disguised_objectObjectAssociationMap则以key-value的形式对应存储在associations中也就是AssociationsHashMap中。

value为空

如果传入的value为空,那么就删除这个关联对象

1
2
3
4
5
6
7
8
9
10
11
12
// 来到这里说明,value为空
// 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);
}
}

表格总结

用表格总结来展示这几个核心类的关系如下

小结

  • 关联对象并不存储在被关联对象本身内存中,而是有一个全局统一的 AssociationsManager
  • 一个实例对象就对应一个ObjectAssociationMap
  • ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation
  • ObjcAssociation中存储着关联对象的valuepolicy策略

objc_getAssociatedObject

objc_getAssociatedObject内部调用的是_object_get_associative_reference

1
2
3
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}

_object_get_associative_reference函数

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
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
// 查找 disguised_object
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
//查看key 和value
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// 存在key 和value 就取出对应的值
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
// 不存在key value 就把这个关联对象擦除
objc_autorelease(value);
}
return value;
}

关键代码已经在上文中给了注释

objc_removeAssociatedObjects函数

objc_removeAssociatedObjects函数用来删除所有关联对象,内部调用了_object_remove_assocations

1
2
3
4
5
6
void objc_removeAssociatedObjects(id object) 
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}

_object_remove_assocations

再来看看_object_remove_assocations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) { // 遍历AssociationsHashMap 取出值
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
// 删除
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}

代码中可以看出,接受一个object对象,然后遍历删除该对象所有的关联对象

总结

用表格总结来展示这几个核心类的关系如下

  • 关联对象并不存储在被关联对象本身内存中,而是有一个全局统一的 AssociationsManager
  • 一个实例对象就对应一个ObjectAssociationMap
  • ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation
  • ObjcAssociation中存储着关联对象的valuepolicy策略
  • 删除的时候接收一个object对象,然后遍历删除该对象所有的关联对象
  • 设置关联对象_object_set_associative_reference的是时候,如果传入的value为空就删除这个关联对象

本文参考资料:

本文相关代码github地址 github

Runtime源码

iOS底层原理