在很多三方库和平时的代码编写中,经常会使用到关联对象,在 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,将会移除这个关联对象。