在很多三方库和平时的代码编写中,经常会使用到关联对象,在 OC
中,一般是在类的分类(Category)中添加关联属性,在 swift
中,一般是在扩展(extension)中添加,当然你也可以在其他地方进行关联属性操作。分类中使用属性,只会做方法的声明。这是因为分类不能直接添加成员变量(可以查看分类的底层结构,并没有设计存放成员变量),但是可以使用关联对象间接达到调用效果。
笔者在看这块知识之前,一直以为关联对象会在运行时加入到类对象的成员变量列表中,然而实际上确不是这样。
关联对象使用 API
// 1.
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
// 2.
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
// 3.
objc_removeAssociatedObjects(id _Nonnull object)
复制代码
- 1.给对象添加关联对象。
object:需要关联值的对象,通常都是 self
key: 取值时使用的键
value: 需要关联的值
policy: 关联策略,和属性的 copy 等等关键字作用一样
// 关联策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // strong nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // copy nonatomic
OBJC_ASSOCIATION_RETAIN = 01401, // strong atomic
OBJC_ASSOCIATION_COPY = 01403 // copy atomic
};
复制代码
- 2.通过
key
获取关联对象的值。 - 3.移除对象的所有关联对象。
用字典实现对象的关联
这里不使用关联对象的 API
,采用一个全局字典来实现对象的关联:
NSMutableDictionary *nickNames;
+ (void)load {
nickNames = [NSMutableDictionary dictionary];
}
- (NSString *)nickName {
return nickNames[@"nickName"];
}
- (void)setNickName:(NSString *)nickName {
nickNames[@"nickName"] = nickName;
}
复制代码
这种实现方式是有问题的,例如内存泄漏等等问题,不过这种思路已经和系统的实现方式很接近了。
关联对象相关数据结构
在查看具体的实现之前,先看看和关联对象相关的数据结构,如果不懂 C++
,下面的源码是很晦涩难懂的,不过可以抓住关键的东西。
- AssociationsManager
class AssociationsManager {
static AssociationsHashMap *_map;
/// ...
};
复制代码
就仅仅存着一个 AssociationsHashMap
类型的成员变量 _map
。
- AssociationsHashMap
typedef ObjcAllocator<std::pair<const disguised_ptr_t, ObjectAssociationMap*> > AssociationsHashMapAllocator;
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
// ...
};
复制代码
这个类的名字中有 HashMap
,可以猜测它就是一个类似于字典的东西,通过键值对的方式储存和获取值,再通过 pair<const disguised_ptr_t, ObjectAssociationMap*>
,可以更加确定了猜测,键就是 disguised_ptr_t
,值就是 ObjectAssociationMap *
。
- ObjectAssociationMap
typedef ObjcAllocator<std::pair<void * const, ObjcAssociation> > ObjectAssociationMapAllocator;
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
// ...
};
复制代码
同上,这里依然又是一个字典,键是 void *
, 值是 ObjcAssociation
。
- ObjcAssociation
class ObjcAssociation {
uintptr_t _policy;
id _value;
};
复制代码
_policy
存着关联策略,_value
存着关联对象的值。
单看这些数据结构依旧是摸不着头脑,接下来查看具体的实现吧。
实现原理
在苹果官方提供的源码中,找到 objc_setAssociatedObject(...)
函数的实现:
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(...)
。不要被这一大片代码吓到,通过上面的数据结构,只需要看懂关键的代码就能理解整个逻辑。
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());
// 1.根据传入的 object 获取 key disguised_object
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// 2.通过 disguised_object 找到储存当前对象的关联对象的 ObjectAssociationMap(可以理解为一个字典)
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
// 3.通过传进来的 key 找到当前对象的 ObjcAssociation
ObjectAssociationMap::iterator j = refs->find(key);
// 4.设置关联值值
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.
// 5.如果传进来的 value 为nil,将会移除关联值
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);
}
复制代码
只要懂了设置关联值的实现,获取关联值和移除关联值的实现也不难看懂了,这里就不深入了。从上面的源码可以分析出,系统就是用的一个全局 AssociationsManager
通过嵌套的两个哈希表储存着所有对象的关联属性。第一个哈希表 AssociationsHashMap
存着所有有关联对象的对象,第二个哈希表存着某个对象的所有关联对象,因此可以知道为什么需要两个哈希表来存了。
总结
关联对象并不是存储在被关联对象本身的内存中,而是存在一个全局的 AssociationsManager
中。
当设置关联属性的值为 nil
,将会移除这个关联对象。