Objective-C 源码(五) Associated Objects 的实现原理

    原文链接:http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/  


    我们知道,在 Objective-C 中可以通过 Category 给一个现有的类添加属性,但是却不能添加实例变量,这似乎成为了 Objective-C 的一个明显短板。然而值得庆幸的是,我们可以通过 Associated Objects 来弥补这一不足。

   

    在阅读本文的过程中,读者需要着重关注以下三个问题:

  1. 关联对象被存储在什么地方,是不是存放在被关联对象本身的内存中?

  2. 关联对象的五种关联策略有什么区别,有什么坑?

  3. 关联对象的生命周期是怎样的,什么时候被释放,什么时候被移除?

    

    与 Associated Objects 相关的函数主要有三个,我们可以在 runtime 源码的 runtime.h 文件中找到它们的声明:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_EXPORT void objc_removeAssociatedObjects(id object)

    这三个函数的命名很容易看懂:

  • objc_setAssociatedObject 用于给对象添加关联对象,传入 nil 则可以移除已有的关联对象;

  • objc_getAssociatedObject 用于获取关联对象;

  • objc_removeAssociatedObjects 用于移除一个对象的所有关联对象。

    注objc_removeAssociatedObjects 函数我们一般是用不上的,因为这个函数会移除一个对象的所有关联对象,将该对象恢复成“原始”状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给 objc_setAssociatedObject 函数传入 nil 来移除某个已有的关联对象。

    

    Key值:

    关于前两个函数中的 key 值是我们需要重点关注的一个点,这个 key 值必须保证是一个对象级别(为什么是对象级别?看完下面的章节你就会明白了)的唯一常量。一般来说,有以下三种推荐的 key 值:

  1. 声明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作为 key 值;

  2. 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作为 key 值;

  3. selector ,使用 getter 方法的名称作为 key 值。

     推荐使用第三种,好用,省代码量。

    

    关联策略

    关联策略                                                        等价属性                                    说明OBJC_ASSOCIATION_ASSIGN                       @property (assign) or                 弱引用关联对象

                                                                  @property (unsafe_unretained)

OBJC_ASSOCIATION_RETAIN_NONATOMIC    @property (strong, nonatomic)    强引用关联对象,且为非原子操作

OBJC_ASSOCIATION_COPY_NONATOMIC       @property (copy, nonatomic)      复制关联对象,且为非原子操作

OBJC_ASSOCIATION_RETAIN                        @property (strong, atomic)         强引用关联对象,且为原子操作

OBJC_ASSOCIATION_COPY                           @property (copy, atomic)           复制关联对象,且为原子操作

    原子性问题不在这里讨论,下面主要讨论前三种形式:

    

    实现原理

    代码Github原文链接:https://github.com/leichunfeng/AssociatedObjects 

    从测试代码中可以看出:

  1. 关联对象的释放时机与被移除的时机并不总是一致的,比如上面的 self.associatedObject_assign 所指向的对象在 ViewController 出现后就被释放了,但是 self.associatedObject_assign 仍然有值,还是保存的原对象的地址。如果之后再使用 self.associatedObject_assign 就会造成 Crash ,所以我们在使用弱引用的关联对象时要非常小心;

  2. 一个对象的所有关联对象是在这个对象被释放时调用的 _object_remove_assocations 函数中被移除的。

    打开 objc-references.mm,找到 objc_setAssociatedObject 函数最终调用的函数;

    

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);
}


    

    在 objc-references.mm 的 objc_getAssociatedObject 函数最终调用了的函数:

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) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}

   

    objc_removeAssociatedObjects

 这个函数负责移除一个对象的所有关联对象,具体实现也是先根据对象的地址获取其对应的 ObjectAssociationMap 对象,然后将所有的关联结构保存到一个 vector 中,最终释放 vector 中保存的所有关联对象。根据前面的实验观察到的情况,在一个对象被释放时,也正是调用的这个函数来移除其所有的关联对象。


    给类对象添加关联对象

    看完源代码后,我们知道对象地址与 AssociationsHashMap 哈希表是一一对应的。那么我们可能就会思考这样一个问题,是否可以给类对象添加关联对象呢?答案是肯定的。我们完全可以用同样的方式给类对象添加关联对象,只不过我们一般情况下不会这样做,因为更多时候我们可以通过 static 变量来实现类级别的变量。我在分类 ViewController+AssociatedObjects 中给 ViewController 类对象添加了一个关联对象 associatedObject 。

    总结

  1. 关联对象与被关联对象本身的存储并没有直接的关系,它是存储在单独的哈希表中的;

  2. 关联对象的五种关联策略与属性的限定符非常类似,在绝大多数情况下,我们都会使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的关联策略,这可以保证我们持有关联对象;

  3. 关联对象的释放时机与移除时机并不总是一致,比如实验中用关联策略 OBJC_ASSOCIATION_ASSIGN 进行关联的对象,很早就已经被释放了,但是并没有被移除,而再使用这个关联对象时就会造成 Crash 。




你可能感兴趣的:(Objective-C 源码(五) Associated Objects 的实现原理)