iOS Objective-C 关联对象

iOS Objective-C 关联对象

1. 关联对象简介

对于关联对象,我们熟悉它的地方就是给分类添加属性。虽然我们可以在分类中通过@property编写代码来声明一个属性,但是当我们使用的时候就报方法找不到错误,其实缺失的方法就是属性的gettersetter的实现,那么关联对象就可以完美的解决这个问题。

官方定义:

Associative references, available starting in OS X v10.6, simulate the addition of object instance variables to an existing class. Using associative references, you can add storage to an object without modifying the class declaration. This may be useful if you do not have access to the source code for the class, or if for binary-compatibility reasons you cannot alter the layout of the object.

Associations are based on a key. For any object you can add as many associations as you want, each using a different key. An association can also ensure that the associated object remains valid for at least the lifetime of the source object.

译: 从OS X v10.6开始可用的关联引用模拟了将对象实例变量添加到现有类中。使用关联引用,可以在不修改类声明的情况下将存储添加到对象。如果您无权访问该类的源代码,或者由于二进制兼容性原因而无法更改对象的布局,则这可能很有用。

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

通过苹果官方文档我们可以知道,关联引用不仅仅可以用在给分类添加属性。但是给分类添加属性是我们最常用的场景。

关联对象的两个函数

  • 设置关联对象
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
  1. 参数一:id object : 要关联的对象
  2. 参数二:const void *key : 关联使用的key值
  3. 参数三:id value : 关联的值,也就是我们要设置的值。
  4. 参数四:objc_AssociationPolicy policy : 策略属性,以什么形式保存
  • 获取关联对象
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}
  1. 参数一:id object : 获取哪个对象里面的关联的值
  2. 参数二:const void *key : 关联使用的key值,通过这个key取出对应的值

关联使用的策略:

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
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. */
};
策略 对应 @property 描述
OBJC_ASSOCIATION_ASSIGN (assign)或者(unsafe_unretained) 指定一个关联对象的弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC (nonatomic, strong) 不能原子化的强引用
OBJC_ASSOCIATION_COPY_NONATOMIC (nonatomic, copy) copy引用,不能原子化
OBJC_ASSOCIATION_RETAIN (atomic, strong) 原子化的强引用
OBJC_ASSOCIATION_COPY (atomic, copy) 原子化的copy引用

2. 关联对象的应用

举个例子,说了半天关联对象可以为分类添加属性,那么我们就把这个例子写一下。

@interface CTObject (Category)

@property (nonatomic, copy) NSString *cate_p1;

@end
@implementation CTObject (Category)
- (void)setCate_p1:(NSString *)cate_p1{
    
    objc_setAssociatedObject(self, @"cate_p1",cate_p1, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

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

3. 关联对象的底层原理

上面两节对关联对象做了简单的介绍和其使用的举例,下面我们来研究一下它的底层实现。

3.1 objc_setAssociatedObject

我们先看看objc_setAssociatedObject的源码,由于使用了各种C++的语法和嵌套,嵌套过程就不多说了,以下是嵌套的代码:

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

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

static ChainedHookFunction SetAssocHook{_base_objc_setAssociatedObject};

由以上代码我们可以知道objc_setAssociatedObject实际调用的是_object_set_associative_reference函数,下面我们就来到_object_set_associative_reference看看它究竟是如何实现的。

_object_set_associative_reference 源码:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    DisguisedPtr disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

acquireValue 源码:

inline void acquireValue() {
        if (_value) {
            switch (_policy & 0xFF) {
            case OBJC_ASSOCIATION_SETTER_RETAIN:
                _value = objc_retain(_value);
                break;
            case OBJC_ASSOCIATION_SETTER_COPY:
                _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
                break;
            }
        }
    }
  • 首先还是做一些非空判断防止一些空对象空值可能会引起的崩溃
  • 判断类是否禁用了关联引用,如果是就打印错误信息
  • 初始化一个disguised对象,是对object按位取反
  • 初始化一个ObjcAssociation对象用于持有关联对象
  • 通过acquireValue函数给我们的value返回一个新值,acquireValue源码在上面,主要是根据策略进行不同的处理
  • 接下来就是初始化一个AssociationsManager对象,获取一个AssociationsHashMap哈希表
  • 接下来分两个流程一个是值存在此时是赋值
    • 首先获取到类的关联表
    • 如果没获取到说明我们是第一次给该类关联,所以需要创建一个新的表
    • 接下来获取表的首地址,并判断对应的key是否已经存在,不存在就直接写入
    • 存在就用新值替换旧值
  • 第二种是值为空,此时是删除关联对象
    • 首先是获取到该类对应的哈希表
    • 判断表不为空
    • 找到key对应的节点
    • 节点不为空判断
    • 替换节点值为空
    • 清空节点
    • 清空节点后,如果表也为空,则清空表
  • 最后释放旧值

3.2 objc_getAssociatedObject

objc_getAssociatedObject就没有那么多嵌套了,直接就可以看出是调用的_object_get_associative_reference函数。

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

_object_get_associative_reference 源码:

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

_object_get_associative_reference的实现也很简单:

  • 首先还是初始化一个ObjcAssociation对象,AssociationsManager对象,获取AssociationsHashMap哈希表
  • 获取到当前对象的关联表
  • 如果表不为空则通过key在表中查找数据
  • 如果找到了并且不为空则调用retainReturnedValue函数根据策略赋值
  • 最后返回通过autoreleaseReturnedValue函数根据策略处理的值

3.3 objc_removeAssociatedObjects

对于关联对象其实还有一个函数objc_removeAssociatedObjects,只不过我们基本不用他,根据名字我们就可以知道该函数是移除关联对象的。这里也嵌套了一层代码,最终调用的是_object_remove_assocations

objc_removeAssociatedObjects 源码:

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

_object_remove_assocations 源码:

// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }

    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

根据注释我们可以知道_object_remove_assocations函数是会对性能有影响的。

  • 这里还是要初始化ObjectAssociationMap对象,AssociationsManager对象,AssociationsHashMap对象。
  • 找出要释放对象的关联表
  • 判断不为空,则将移除这些关联关系,并释放
  • 最后循环释放类的所有关联表

4. 总结

  • 关联对象实际上在底层是一个ObjcAssociation对象结构
  • 全局由一个AssociationsManager管理类存储了一个静态的哈希表AssociationsHashMap
  • 这个哈希表存储的是以对象指针为键,以该对象所有关联对象为值
  • 关联对象又是以ObjectAssociationMap来存储的
  • ObjectAssociationMap的存储结构以key为键,ObjcAssociation为值
  • 判断一个对象是否存在关联对象可以通过对象isahas_assoc

至此我们的关联对象就基本分析完毕了,但是由于本人才疏学浅,有些地方用词不当,一些C++语法不是很熟悉,有些表述不完整,不贴切,但是我也想不出什么好词的,可能也会有些不准确。如有问题欢迎指正。

5. 参考资料

**Apple Associative References
**

你可能感兴趣的:(iOS Objective-C 关联对象)