iOS weak源码之表中表

我们都知道,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& SideTables() {
    return *reinterpret_cast*>(SideTableBuf);
}

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

StripedMap为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>(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(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 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 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 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源码之表中表)