美文网首页iOS 源码解析
iOS weak源码之表中表

iOS weak源码之表中表

作者: 雨三楼 | 来源:发表于2020-09-21 14:00 被阅读0次

    我们都知道,weak的主要作用是为了防止循环引用,而产生循环引用的根本原因则在于ARC下的引用计数错误问题,即两个对象或者多个对象相互持有,会造成超出作用域后引用计数不会减为0的现象。而weak和strong不同,它并不会增加对象的引用计数。

    循环引用在ARC下,是不可避免的,于是weak也就应运而生了,与其说weak是弱引用,倒不如说weak是独立于引用计数之外的内存管理机制。

    常见的weak使用场景一般有如下两个:

    __weak wObj = obj;
    @property(nonatomic,weak)id obj;
    

    实际上这两个是一样的,写法不同而已,但是还是稍微有一点点区别,经过断点调试发现,__weak wObj = obj;方法在运行时调用的是objc_initWeak@property(nonatomic,weak)id obj;则调用的是objc_storeWeak

    ​ 这两者的区别在于下一步调用的storeWeak方法中old参数的不同,所以答案也显而易见,我们使用属性的时候,编译器会在类初始化的时候,完成属性里成员变量的声明,从而调用objc_initWeak.

    当我们使用weak的时候,实际上runtime会自动调用storeWeak函数,查询源码我们会发现整个weak的实现历程。

    未命名文件

    大致的调用过程很简单,并没有什么出奇的地方,然而当我们下潜到具体的数据结构和函数实现的时候,才能感觉到weak设计的美妙之处。

    全局散列表

    调用完主函数 storeWeak之后,第一个出现的函数是&SideTables()这样一个很怪的C++函数:

    static StripedMap<SideTable>& SideTables() {
        return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
    }
    

    本函数的含义在于获取StripedMap<SideTable>类型的全局静态变量。

    StripedMap<SideTable>为C++的模板类,类似于我们常用的泛型。

    StripedMap类的实现非常有意思。

    让我们回到主函数内,oldTable = &SideTables()[oldObj];调用完&SideTables之后,紧接着一个中括号是什么操作?C++里貌似只有数组才能用中括号,进入StripedMap类中看一下,发现如下操作:

        T& operator[] (const void *p) { 
            return array[indexForPointer(p)].value; 
        }
        const T& operator[] (const void *p) const { 
            return const_cast<StripedMap<T>>(this)[p]; 
        }
    

    原来是运算符重载,PaddedT array[StripeCount];内部实际上获取了array这个数组的成员。

    至于数组类型,当然就是我们的SideTable

     struct PaddedT {
            T value alignas(CacheLineSize);
        };
    

    alignas作用:内存对齐。

    获取下标的方式(散列函数的实现):中括号中的oldObj在这里是const void *p,可以看到这里的p是一个的指针。

    oldObj是id类型,即objc_object *类型,是一个指向objc_object的指针.

    利用p指针,入indexForPointer的参:

    static unsigned int indexForPointer(const void *p) {
            uintptr_t addr = reinterpret_cast<uintptr_t>(p);
            return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
        }
    

    StripeCount为64 ,利用p的地址对64取余的意义在于既能防止数组越界,又能保证同一个地址获取的下标一致(确定性和散列碰撞)。

    至此&SideTables()函数的实现分析完毕,查找一个长度为64的全局的散列表,获取SideTable。

    SideTable

    主函数继续执行,执行到两个关键函数:

    weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,(id)newObj, location,CrashIfDeallocating);
    

    主要用到的是SildeTable中的weak_table,而weak_table则又是一个hash表,里面存储的weak_entry_t真正存储了newObj实体DisguisedPtr<objc_object> referent;

    这两个关键函数的意义就是将旧的obj从SildeTable中移除,再将新的obj加入SildeTable中。

    而两个函数实现的关键点在于获取weak_table中的weak_entry_t实体,调用的同一个方法weak_entry_for_referent:

    static weak_entry_t *
    weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
    {
        assert(referent);
    
        weak_entry_t *weak_entries = weak_table->weak_entries;
    
        if (!weak_entries) return nil;
            // 散列函数
        size_t index = hash_pointer(referent) & weak_table->mask;
        size_t hash_displacement = 0;
        // 查找
        while (weak_table->weak_entries[index].referent != referent) {
            index = (index+1) & weak_table->mask;
            hash_displacement++;
            if (hash_displacement > weak_table->max_hash_displacement) {
                return nil;
            }
        }
        
        return &weak_table->weak_entries[index];
    }
    

    将查找到的weak_entry返回。

    weak_entry_t

    weak_entry_t的结构如下:

    struct weak_entry_t {
        DisguisedPtr<objc_object> referent;
        union {
            struct {
                weak_referrer_t *referrers;
                uintptr_t        out_of_line : 1;
                uintptr_t        num_refs : PTR_MINUS_1;
                uintptr_t        mask;
                uintptr_t        max_hash_displacement;
            };
            struct {
                // out_of_line=0 is LSB of one of these (don't care which)
                weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
            };
        };
    };
    

    第一个成员DisguisedPtr<objc_object> referent;就是我们主函数里id类型的对象(对象被保存在了这里)。

    第二个成员是c++里的共用体,我们先看看它储存的是什么,在weak_register_no_lock函数中有对weak_entry_t的初始化代码:

    weak_entry_t new_entry;
    new_entry.referent = referent;
    new_entry.out_of_line = 0;
    new_entry.inline_referrers[0] = referrer;
    for (size_t i = 1; i < WEAK_INLINE_COUNT; i++) {
        new_entry.inline_referrers[i] = nil;
    }
    
    weak_grow_maybe(weak_table);
    weak_entry_insert(weak_table, &new_entry);
    

    共用体储存的是referrer,它就是主函数中的location,即我们用__weak修饰的对象指针,这里的作用是在对象释放的时候,也能查找到与之关联的weak修饰的对象指针,并对其进行置空操作,防止野指针。

    所以我们用多个weak指向同一个对象的操作,是非常安全的。

    weak释放

    由于weak的内存管理游离于arc/mrc之外,故在arc生效并且没有错误操作的情况下,全局的散列表仍然持有对象的引用。

    所以在每个对象调用dealloc方法的时候,会对当前对象所在的weak表进行clear操作,具体函数如下:

    void 
    weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
    {
        objc_object *referent = (objc_object *)referent_id;
    
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
        if (entry == nil) {
            /// XXX shouldn't happen, but does with mismatched CF/objc
            //printf("XXX no entry for clear deallocating %p\n", referent);
            return;
        }
    
        // zero out references
        weak_referrer_t *referrers;
        size_t count;
        
        if (entry->out_of_line) {
            referrers = entry->referrers;
            count = TABLE_SIZE(entry);
        } 
        else {
            referrers = entry->inline_referrers;
            count = WEAK_INLINE_COUNT;
        }
        
        for (size_t i = 0; i < count; ++i) {
            objc_object **referrer = referrers[i];
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
                else if (*referrer) {
                    _objc_inform("__weak variable at %p holds %p instead of %p. "
                                 "This is probably incorrect use of "
                                 "objc_storeWeak() and objc_loadWeak(). "
                                 "Break on objc_weak_error to debug.\n", 
                                 referrer, (void*)*referrer, (void*)referent);
                    objc_weak_error();
                }
            }
        }
        
        weak_entry_remove(weak_table, entry);
    }
    

    这么长的一段代码,其实只有两个操作:

    1.*referrer = nil;将weak指针置空,防止野指针。

    2.weak_entry_remove(weak_table, entry);删除entry,并且对内存清空。

    至此,对象和weak表的关系也就荡然无存了,对象完成了释放,weak表的中的weak引用通过对象的地址在两个散列表中查找到后,同样完成了释放。

    总结

    简单来说,arc下的引用操作往往伴随着引用计数的变化,而引用计数又绕不开循环引用这个诟病,所以weak其实就是一种不引起引用计数变化的"弱引用"机制。

    但从weak设计的数据结构而言,可以分为最外层的全局StripedMap表,存储其中的SlideTable表,而SlideTable中,又有weak_referrer_t数组,来存储weak引用,多级结构设计,也让源码的阅读更有意思。

    相关文章

      网友评论

        本文标题:iOS weak源码之表中表

        本文链接:https://www.haomeiwen.com/subject/yiruxktx.html