iOS 关联对象 objc_setAssociatedObject ,从源码探讨原理,以及释放时机

iOS 关联对象 objc_setAssociatedObject ,从源码探讨原理,以及释放时机

1.objc_setAssociatedObject

直接上源码,当我们在 OC 层调用set方法的时候,源码直接会调用另个方法


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


void _object_set_associative_reference(id object, 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;
    
    assert(object);
    
    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));
    
    // 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);
}

下面我们来分析下源码

首先


id new_value = value ? acquireValue(value, policy) : nil;

会调用


static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return objc_retain(value);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}

是来判断当前的策略的,就是我们外面会穿进来一个参数,用来标识是引用计数加一还是copy,接着


AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);

接着拿着 AssociationsManager


 AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

取出 hashMap,也就是 AssociationsHashMap disguised_ptr_t disguised_object = DISGUISE(object); 拿到当前对象的补码,计算机能够识别的补码,跟着思路继续往下走,看代码


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

这段代码什么意思呢?我们上边拿到了全局的 AssociationsHashMap 这里面是以每个对象的指针按照16位取反得到补码,作为键,然后以ObjectAssociationMap作为value存储的,那么ObjectAssociationMap又是什么呢,


AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;

这段代码是通过补码找到 ObjectAssociationMap ,然后,


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

从这段代码我们可以猜到,ObjectAssociationMap 中,是以传入的key作为key,然后以ObjcAssociation作为 value,而 ObjcAssociation 存储的是策略和我们的value,注意我们的value在上面处理过,进行过引用计数加一或者copy操作。源码这个额地方做的就是如果没有就创建一个存进去,如果有就做一下替换。如果看不明白,就从源码的 else 语句,一看便知

else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }

如果从我们的 hashmap 中没有找到 ,那么久新创建一个 ObjectAssociationMap ,然后当前全局的 hashMap 的 key为当前对象指针做的补码,你可以理解为以当前对象作为key,然后 以新建的这个 map作为value,然后,这个mao,又以当前的传进来的 key 作为key,以ObjcAssociation作为value,ObjcAssociation 存储着策略和key对应的value,也是传进来的。

不要忽略一个逻辑,就是上面的 如果 new_value 为 nil,


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


如果传进来的值为 nil,就会找到对应的 map,然后找到对应的 association 清除,所以说,如果我们给我们的关联对象设置为nil,就就是做了清除操作

我们再来看下 AssociationsManager,

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

AssociationsManager 是全局唯一的,一次只能有一个manager,后面可以知道,里面有一个静态指针 AssociationsHashMap,全局只有一份,程序已启动就会 有一份,无论我们初始化了几个manager,然后我们看他的构造函数和析构函数,是做了一个锁,来保证 manager 全局只有一份,初始化的时候加锁了,然后在想初始化是不行的,只能等析构函数,系统应该是为了保证唯一性,也就是说我们如果正在设置关联对象,这时候又来设置是会被锁拦截的。

1.1总结


1. 首先根据传进来的关联策略,来对传进来的 value 进行处理,策略也是我们穿进去的,是否进行copy
2. 然后初始化全局 `AssociationsManager`,上面分析过我们的manager,如果这时候有另一个对象来初始化做关联对象,那么他是会被阻塞的,阻塞在构造函数,当我们这次初始化完成,调用了析构函数,下一次才会进行。
3. 然后取出manager中的全局唯一的一份 hashMap
4. 然后调用函数,获取当前对象的反码
5. 然后判断根据我们传进来的value和策略生成的新value是否存在
6. 如果存在value,从 hashMap 中,根据反码作为 key ,取出 ObjectAssociationMap
7. 如果找到 map,然后根据我们传入的 key 作为 key、,找到 ObjcAssociation对象
8. 如果 7 找到了 ObjcAssociation,那么就用新值替换旧的值
9. 如果 7 没有找到,就新建一个 ObjcAssociation 对象,以我们的key和value构造,然后以传进来的key,存储到 ObjectAssociationMap 中
10. 如果 6 没有找到,ObjectAssociationMap,就会新建一个 ObjectAssociationMap ,然后根据我们传进来的 key 和 value 生成新的ObjcAssociation对象,然后存储进去
11. 如果5中发现我们传进来的value为nil,就一步步找,如果找到了存储的ObjcAssociation,就将其销毁

2._object_get_associative_reference



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

上面的代码我就不分析了,因为前面已经分析过set,get很简单,只是,需要根据我们设置的策略判断是否需要retail和release

3.释放时机

前面我们已经讲了set 和 get ,那么重头戏就是,关联对象什么时候释放呢?如果你看过我前面的 weak 释放时机 ,你就会明白的,你可以想想一下,我们只有设置关联属性,什么时候去释放过他,那么系统是怎么做的呢?

从我们观看源码可以知道,在我们的对象进行 dealloc 的时候,

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

回去判断我们的 isa 指针是否有弱引用,关联对象等一系列乱七八糟的东西,isa 指针 在我之前的文章也讲过,所以,当我们有关联对象的时候,会执行object_dispose((id)this);f方法,


object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}


void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

可以看到,有这么一句 if (assoc) _object_remove_assocations(obj); 这个就是自动释放我们的关联对象,


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


代码很容易理解,就是找到 map ,然后循环遍历释放我们设置过得 key value,是不是说到这,豁然开朗了。

4.加餐

上面我们说到过,是根据 isa 指针里面的 字段来判断是否设置过关联对象的,那么是咋么设置到 isa 指针里面的呢?

我们在上面有一个流程是这样的,若果没有 map,会创建map


ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();

赋值完之后,会调用 object->setHasAssociatedObjects();,


inline void
objc_object::setHasAssociatedObjects()
{
    if (isTaggedPointer()) return;

 retry:
    isa_t oldisa = LoadExclusive(&isa.bits);
    isa_t newisa = oldisa;
    if (!newisa.nonpointer  ||  newisa.has_assoc) {
        ClearExclusive(&isa.bits);
        return;
    }
    newisa.has_assoc = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}

设么是 TaggedPointer,我之前文章也讲过,这个地方就是将我们的 isa 指针的 has_assoc 置为1,一直到声明周期结束,我们在dealloc的时候,就是根据这个字段来判断的,是不是又豁然开朗了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350