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<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引用,多级结构设计,也让源码的阅读更有意思。

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