iOS-关联对象

547 阅读5分钟

什么是关联引用,关联对象?

官方给出的解释是:关联引用模拟了将对象实例变量添加到现有类中。使用关联引用,可以在不修改类声明的情况下将存储的数据添加到对象。如果您无权访问类的源代码,或者由于二进制兼容性原因而无法更改该对象的布局,使用关联引用则可以解决这些问题。

关联引用基于密钥。对于任何对象,您都可以根据需要添加任意数量的关联,每个关联都使用不同的键key。关联还可以确保关联的对象至少在源对象的生命周期内保持有效。

关联对象并不是存储在被关联对象本身的内存中,通过分析底层实现,它存储在由AssociationsManager管理的全局统一的一个AssociationsHashMap中。

关联对象有以下4个核心内容:

  • AssociationsManager: 关联对象的管理类
  • AssociationsHashMap: 存储关联对象的HashMap
  • ObjectAssociationMap: 存储确定对象的表
  • ObjecAssociation: 表中存储的最小结构,存储值和策略

关联对象主要有3个API:

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

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

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
  • objc_setAssociatedObject是设置关联对象
  • objc_getAssociatedObject是获取关联对象
  • objc_removeAssociatedObjects是移除关联对象

通过这3个方法,我们就可以实现对对象添加关联,并对关联进行相关的操作。

那么我们先来具体看一下,这3个方法都做了什么:

objc_setAssociatedObject

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    
    if (!object && !value) return;

    ......
    
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    
     {
        // 关联对象的管理类
        AssociationsManager manager;
        // 获取关联的 HashMap -> 存储当前关联对象
        AssociationsHashMap &associations(manager.associations());
        // 对当前的对象的地址做按位去反操作 - 就是 HashMap 的key (哈希函数)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            // 获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                // 根据key去获取关联属性的迭代器
                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).
                // 如果AssociationsHashMap从没有对象的关联信息表,
                // 那么就创建一个map并通过传入的key把value存进去
                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.
            // 如果传入的value是nil,并且之前使用相同的key存储过关联对象,
            // 那么就把这个关联的value移除(这也是为什么传入nil对象能够把对象的关联value移除)
            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);
}

该方法需要的参数:

  • id object: 需要关联的对象
  • void *key: 给关联的表中设置的key
  • id value: 关联的属性
  • uintptr_t policy: 关联策略

关联策略的选项如下:

  • OBJC_ASSOCIATION_ASSIGN = 0: 弱引用
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1: 强引用,且nonatomic
  • OBJC_ASSOCIATION_COPY_NONATOMIC = 3: copy、nonatomic
  • OBJC_ASSOCIATION_RETAIN = 01401: 强引用,且atomic
  • OBJC_ASSOCIATION_COPY = 01403: copy、atomic

通过以上的代码,我们可以看出AssociationsManager管理着所有的AssociationsHashMap,首先判断传入的value是否是nil,

  • 如果是nil,则看能否找到object表对应的ObjectAssociationMap,找到的话就把原来存的值置空。(解除关联)
  • 如果不是nil,则通过传入的object找到对应的表,
    • 如果找到,然后通过传入的key来找对应的值,这个值是一个map,存着value和策略
      • 如果找到就对其进行处理,设置新值
      • 如果找不到,就创建一个新的map,存入value和策略。
    • 如果找不到表就说明没有这张表,那就创建一张新表,然后再创建一个新的map,存入value和策略。

objc_getAssociatedObject

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


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);
        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()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

该方法需要的参数如下:

  • id object: 需要关联的对象
  • void *key: 给关联的表中设置的key

同理,先通过AssociationsManager拿到存放所有hashAssociationsHashMap,然后根据传入的object生成的地址找到相关的ObjectAssociationMap表,

  • 如果找到,然后通过传入的key来找对应的ObjcAssociation
    • 如果找到就对其进行处理,然后返回value
    • 如果找不到,就返回nil
  • 如果找不到,则返回nil

objc_removeAssociatedObjects

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

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()) {
            // 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());
}

该方法只需要传入一个参数:id object,为需要删除关联的对象。

同理,先通过AssociationsManager拿到存放所有hashAssociationsHashMap,然后根据传入的object生成的地址找到相关的ObjectAssociationMap表,然后将该表中的值存到一个临时变量elements中,该表中的所有值。最后释放elements中的数据。

了解了关联对象,我们来看看它有什么具体的用处。

给分类添加属性

首先我们创建一个TPerson类:

@interface TPerson : NSObject
@property (nonatomic, copy) NSString *name;

@end

#import "TPerson.h"

@implementation TPerson

+ (void)load {
    NSLog(@"类-load");
}

@end

并且为其创建一个分类:

#import "TPerson.h"

@interface TPerson (addition)
@property (nonatomic, copy) NSString *cateProp;

@end

@implementation TPerson (addition)

+ (void)load {
    NSLog(@"分类-load");
}

@end

如果给TPerson添加在TPerson的分类中添加了一个cateProp的字符串属性,当我们在main函数中运行如下代码,就会崩溃:

TPerson *per = [TPerson alloc];
per.cateProp = @"分类的属性";
NSLog(@"---%@----", per.cateProp);

-[TPerson setCateProp:]: unrecognized selector sent to instance提示我们TPerson没有为cateProp实现set方法,那么我们如何实现它的set/get方法呢?

使用关联引用,我们就可以实现set/get方法:

- (void)setCateProp:(NSString *)cateProp {
    objc_setAssociatedObject(self, @"cateProp", cateProp, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

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

这样分类的cateProp属性就可以正常使用了。

总结