浅析关联引用

Posted 芒果味ly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析关联引用相关的知识,希望对你有一定的参考价值。

背景

今日吃饱了,确实撑得慌,找了我的邻居-阿杰一起散步,走了好大一圈,最后在小区下聊起了技术,从YYKit,SDWebImage 等第三方库,扯到了关联引用,因为他们都用到了这个技术,然而我又想到了单例,单例和关联引用在实现上有一个相同点—都需要一个静态变量;那么疑问就来了:同样都需要一个静态变量,为什么结果不一样呢?或许你还没明白我的疑问是什么,请继续阅读吧!

一、回顾单例

我简单的写了个单例类 SingletonObject ,把静态变量 instance 放在 sharedInstance 类方法里看起来可能更好一些,放在外面也是可以的,二者的区别是作用域不同,生命周期也差不多,我个人比较喜欢放在方法内部,因为外部一般不需要直接获取,这里放在外部是为了和我们今天的角儿进行对比,提出我的疑问做铺垫。

@implementation SingletonObject

static id instance;

+ (instancetype)sharedInstance

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^
        instance = [[self alloc]init];
    );
    return instance;


@end

请问你有没有思考过,为什么这里可以实现单例,instance 变量不是每个对象都有一份吗?当然不是的!这个 instance 静态变量的地址是和类 SingletonObject 是对应的,也就说 instance 静态变量的地址是确定的,只要这个类的地址确定了,他也就是确定的了,因此可以简单的理解为 instance 静态变量是属于类的,姑且称之为 “类变量”,与实例变量相对应。

二、提出疑问

我们已经知道了静态变量是个“类变量”,我们也都用过 SDWebImage,其内部实现正是使用了关联引用,才使得我们使用起来是那么的方便,图片可以准确无误的下载显示出来,也就是说每个 imageView 对应图片都是自己想要的,一一对应的,我的问题来了,既然关联引用也使用了“类变量”,那么不应该是 imageView 最终都要显示为同一张图片才对嘛?!虽然你创建了 N 个 imageView ,但是 imageView 的 “类变量”(关联引用的 key) 是唯一的啊,这明显是个一对多的关系!可为什么使用 SDWebImage 的时候一切都那么正常,最终图片都一一对应上了,你怎么看呢?我觉得此事必有蹊跷!

三、大胆推测

我最后给阿杰做了一个这样的推测:问题肯定在关联引用的处理上,要不然我们就没必要在使用的时候传个 key 进去了,或许正是根据这个 key 做了一个地址偏移什么的,每个对象都能或许元类信息,然后给自身加个偏移存储下这个关联的对象,我们可以从打印静态变量地址,打印堆栈信息,runtime源码入手…

然后就各回各家,各找各妈了…

四、揭开神秘面纱

这么大的疑问放心里,肯定睡不着的,回家就开始找源码了,结果就找到了内部实现的代码,现在拿出来仔细分析下内部实现:

//这是我们调用的方法
void objc_setAssociatedObject(id object, const void *key, id value, 
                         objc_AssociationPolicy policy) 

    objc_setAssociatedObject_non_gc(object, key, value, policy);

//这是内部的私有方法;
void objc_setAssociatedObject_non_gc(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) 
    // 这段代码有点像重写 setter 方法的感觉;old就是赋值之前的老值,要被释放掉的;ObjcAssociation 下面会介绍;
    ObjcAssociation old_association(0, nil);
    // acquireValue:对传入的 value 做 reatain,copy 等策略
    id new_value = value ? acquireValue(value, policy) : nil;
    
        AssociationsManager manager;//这是重点,每个类都可以创建一个 manager 管理关联的对象,不过 manager 管理的 HashMap 一直都是一个,并且是线程安全的!
        AssociationsHashMap & (manager.associations());//获取唯一的 map 引用;
        disguised_ptr_t disguised_object = DISGUISE(object);//把对象伪装下,说得直白些就是将对象的地址转化为一个 unsigned long 的整型;
        if (new_value)  //如果新值不是空的
            // 这里是 c++ 语法,按照 key 查找 map 里的键值对,返回的是一个 iterator;
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end())  //说明找到了对象对应的 map 了;
                ObjectAssociationMap *refs = i->second; //取 value,first 是取 key http://www.cplusplus.com/reference/unordered_map/unordered_map/
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end())   //说明在对象对应的 map 里找到了 key 对应的 map ;
                    old_association = j->second; //取值赋给 old,后面会释放;
                    j->second = ObjcAssociation(policy, new_value); //赋上新值;这里包装了下,不是直接赋值,可思考下为什么?
                 else  //说明在对象对应的 map 里没能找到 key 对应的 map !
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                
             else  //找不到对象对应的 map ;第一次肯定都找不到,需要创建;
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs; //创建一个 map ,对象是 key;
                (*refs)[key] = ObjcAssociation(policy, new_value); //然后把关联对象放到对象对应的空 map 里;key就是关联引用使用的那个静态变量;
                object->setHasAssociatedObjects(); //做个标记;下次就能够找到了;
            
         else  //如果新值是空的,相当于 set 了 nil ;
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end())  //找到了对象对应的 map 了;
                ObjectAssociationMap *refs = i->second; //取出对象对应的 map
                ObjectAssociationMap::iterator j = refs->find(key); //查找 key 对应的 ObjcAssociation
                if (j != refs->end()) 
                    old_association = j->second; //取值赋给 old,后面会释放;
                    refs->erase(j); //从 map 里擦除;
                
            
        
    
    // 释放 old
    if (old_association.hasValue()) ReleaseValue()(old_association);

仔细阅读完源码后,疑问马上没了,也挺好理解的,只不过需要一点 c++ 的知识,基本每一行都加了注释,这里就不详细说了,做个简单的总结:

  1. 使用AssociationsManager类管理所有类关联的对象,其内部用静态的(也就是唯一的) AssociationsHashMap 存储,map 存的是键值对,这里的键就是这个类的对象(需要处理下),值是 ObjectAssociationMap。(这两个 map 有着不同的父类)简单看下AssociationsManager:
class AssociationsManager 
    static spinlock_t _lock; //还用到了自旋锁
    static AssociationsHashMap *_map; // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()    spinlock_lock(&_lock); 
    ~AssociationsManager()   spinlock_unlock(&_lock); 

    AssociationsHashMap &associations()  //c++ 的引用,好难理解清楚引用和指针
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    
;

2.每个对象的ObjectAssociationMap里存的当然也是键值对,这里的键就是你定义的关联对象的 key,值是 ObjcAssociation ,这里做了一次包装,看下他的源码:

class ObjcAssociation 
        uintptr_t _policy; //存储策略,这个在销毁对象的时候需要用到;
        id _value;//被包装的对象
    public:
        ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) 
        ObjcAssociation() : _policy(0), _value(nil) 

        uintptr_t policy() const  return _policy; 
        id value() const  return _value; 

        bool hasValue()  return _value != nil; //还记得上面用到这个方法了吗?
    ;

3.明白了setter方法之后,很容易理解getter方法,简单看下objc_getAssociatedObject内部的细节:

//这是我们调用的方法
id objc_getAssociatedObject(id object, const void *key) 

    return objc_getAssociatedObject_non_gc(object, key);


//这是内部的私有方法;
id objc_getAssociatedObject_non_gc(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();
                //上面的还是那两步,最后根据key找到对应的关联对象;这里冒出来一个 getter policy,倒是很少遇见!或许在 copy 对象的时候才会遇见吧...
                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_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARR ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
* Be warned that GC DOES NOT CALL THIS. If you edit this, also edit finalize.
* CoreFoundation and other clients do call this under GC.
**********************************************************************/
void *objc_destructInstance(id obj) 

    if (obj) 
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);//移除关联的对象
        if (dealloc) obj->clearDeallocating();
    

    return obj;

下面看下 _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);//Add element at the end //http://www.cplusplus.com/reference/vector/vector/            
            // 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());

从 manager 里取出类对应的 AssociationsHashMap,然后查找当前对象对应的AssociationsHashMap ;如果找到了,说明这个对象确实使用了关联引用,然后取出对应的 ObjectAssociationMap ;

接着枚举 ObjectAssociationMap ,取出关联的对象,然后放到 vector 的末尾;

最后通过 for_each 操作 vector 里的每一个元素,都去调用 ReleaseValue()方法;

前面已经见过ReleaseValue()这个方法了,这里看下源码:


static void releaseValue(id value, uintptr_t policy) 
    if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) //这就是为何要把关联的对象包装一下的原因!
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_release);//发送release消息;
    


struct ReleaseValue 
    //应该是重载运算符了,我的c++知识忘光了耶;
    void operator() (ObjcAssociation &association) 
        releaseValue(association.value(), association.policy());
    
;

由此看来,我们无需关系关联对象的内存管理,当一个对象释放的时候,会自动去释放被关联的对象!这也正好符合内存管理的法则:谁使用谁管理!

六、验证类变量

我觉得我的猜测应该是靠谱的,因此我打印了类的起始地址和结束地址,各种类变量地址,以及对象地址,结果如下:

2016-03-06 14:17:42.056 StudyAssociationSourceCode[27561:2531350] 
-------------
SingletonObject类
起始地址:0x100002320
结束地址:0x100002328
类方法的地址:0x1000017a0

@implementation之外的静态变量地址:0x100002359
@implementation之内,方法之外静态变量地址:0x10000235a
方法内的静态变量地址:0x100002358

对象的起始地址:0x100600450
对象的结束地址:0x100600458
-------------
2016-03-06 14:17:42.056 StudyAssociationSourceCode[27561:2531350] association key:0x10000235b

1.可以看出这几个的地址是连续的:

0x100002358 :方法内的静态变量地址
0x100002359 :@implementation之外的静态变量地址
0x10000235a :@implementation之内,方法之外静态变量地址
0x10000235b :@implementation之外的 association key 静态变量地址【不同的文件】

这说明类变量不管写到哪里都是放到了一起的,都在一个区域内;

2.这几个的内存空间是相近的:

0x1000017a0 :类方法的地址
0x100002320 :类起始地址
0x100002328 :类结束地址
0x100002358 :类变量地址
...

这里有些怪异,类方法的地址竟然在类的地址空间之外!要想解释这个现象就需要知道 OC 对象的集成体系、元类、Class结构体等,这里先卖个关子,后续博客介绍。

大体上看,凡是跟类相关的变量,方法的内存都是连续分配的;

3.对比类地址和对象地址:

0x100002320 :类起始地址
0x100600450 :对象的起始地址

可以看出他们的内存空间差的很多,这说明了在内存分配上类和对象是分开的!具体的细节以后验证吧,我现在也不能把他们说明白。

这是我测试工程的地址:https://github.com/debugly/StudyAssociationSourceCode

七、总结

一切都清楚了,对于 “类变量” 的理解也是没错的,简单的说:类变量是属于类的,实例变量是属于对象的,清楚这一点有利于我们在编码过程中正确的使用使用类变量,迅速找到使用类变量埋下坑,类变量属于类这一特性有时候真的可能会给你带来问题,比如你写一个局部静态变量,页面每次进来都修改下这个值,可是你会发现第二次进来的时候他仍旧是上次修改过的,或许这不是你想看到的!

关联引用的实现如此巧妙,元类如此的迷人,这些都需要继续深入学习才能体会得到!苹果把 Runtime 开源了,那么我们就有必要去看看源码,即使我们不能为之作出什么贡献,但至少可以提升自身,对于知识体系的形成都有很大帮助!

不足之处请多多指正!

以上是关于浅析关联引用的主要内容,如果未能解决你的问题,请参考以下文章

浅析关联引用

Rails引用相同模型的活动记录关联

word内容相同的文字关联

当大厂螺丝钉,还是二线公司的“角儿”?

浅析MySQL多次查询和关联查询的效率问题

浅析MySQL多次查询和关联查询的效率问题