iOS底层-- weak修饰对象存储原理

问题:为何weak修饰的变量可以打破循环引用?
因为weak修饰的变量存储在散列表中的弱引用表里,不参与引用计数器的使用,也就是说,在进行释放额时候,不管你怎么引用,直接就把你置空了。

基本概念

  • SideTable :散列表:系统自动维护,用于存储/管理一些信息
    SideTable的结构中能看到 其管理三种表:
    spinlock_t slock 自旋锁表;
    RefcountMap refcnts 引用计数表;
    weak_table_t weak_table 弱引用表;
  • weak_table:弱引用对象存储的表,是SideTable中的一张表
  • weak_entry_t:weak_table里面的一个单元,用于管理当前类的弱引用对象,可以理解为一个数组,查看weak_entry_t的结构,有助于更好的理解里面的存储结构,里面包含一个weak_referrer_t,相当于一个数组,这里面的就是存储的弱引用对象,还有其他的一些信息,比如mask(蒙版,容量-1)、num_refs (当前存储的数量)等
  • weak_referrer_t :weak_entry_t 中的弱引用对象,相当于是 数组中的一个元素

存储原理

1、源码探索入口

写上这样的代码,打上断点,并打开汇编模式:debug->debug workflow -> alway show disassembly

- (instancetype)init
{
    if (self = [super init]) {        
️        id __weak weakSelf = self;    //断点在这
    }
    return self;
}

运行后会进入断点,出现这样的一些信息


汇编模式信息

找到callq方法:objc_initWeak ,拿到这个方法就可以进入源码去调试了

源码探索

id objc_initWeak(id *location, id newObj)
{
   .....
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>  (location, (objc_object*)newObj);
}

1.1、内部做的操作是 存储weak—storeWeak

static id 
storeWeak(id *location, objc_object *newObj)
{
    ......

    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

//有新值,判断类有没有初始化,如果没有初始化,就进行初始化
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);
            goto retry;
        }
    }

// 有旧值,进行 weak_unregister_no_lock操作
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

// 有新值  进行weak_register_no_lock 操作
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);

        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }
        *location = (id)newObj;
    }
    else {
    }
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

关键步骤:
1、如果cls还没有 初始化,先初始化这个类

2、如果weak对象有旧值,先对旧值 进行 weak_unregister_no_lock,删除旧值
3、如果weak对象有新值 就对新值进行weak_register_no_lock,新增新值

1.2、再来看weak_unregister_no_lock,删除旧值

void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;

    if (!referent) return;
// 在 weak_table 中去找到 有 referent 的 entry (相当于在weak_table 表中去找到包含referent 元素的 数组)
   if ((entry = weak_entry_for_referent(weak_table, referent))) {

// 找到了 这个  entry,  就删除 entry 中的  引用对象-referrer
        remove_referrer(entry, referrer);
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }

// 如果 entry 中的引用对象 没有了 删除这个 entry
        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }
}

关键步骤
1、在 weak_table 中去找到 有 referent-引用对象的 entry (相当于在weak_table 表中去找到包含referent 元素的 数组)
2、如果找到了 entry 就删除 entry中的 referent-引用对象
3、判断 entry 里面 还有没有其他对象,如果没有,就把entry也remove掉(相当于数组中的元素为空,就把这个数据也删掉)

1.3、 存储新值:weak_register_no_lock

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    ......

    weak_entry_t *entry;
// 在 weak_table 中去找到 有 referent 的 entry (相当于在weak_table 表中去找到包含referent 元素的 数组)
    if ((entry = weak_entry_for_referent(weak_table, referent))) {

// 如果找到,直接append
        append_referrer(entry, referrer);
    } 
    else {
// 如果没有找到相应的 entry ,就创建一个entry 并插入 weak_table
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}

关键步骤
1、在 weak_table 中去找 有 referent 的 entry (相当于在weak_table 表中去找到包含referent 元素的 数组)
2、如果找到entry,进行添加操作:append_referrer
- 2.1、如果有空位,直接插进去
---- 这里有一个疑问:为什么会有一个空位?这里可以看new_entry的实现:初始容量为4,并默认4个空值
- 2.2、如果数量超过容量的3/4,进行扩容,再添加(这里想到,方法缓存机制,方法缓存也是超过3/4进行扩容,方法的扩容是:扩容之后,以前的方法删掉了,再把需要缓存的方法插进去)
3、如果没找到entry,创建一个entry,在进行插入


大概的过程是这样的:


大概流程

释放原理

弱引用对象在释放的时候,可以在dealloc中去看具体是怎么释放的
dealloc -》
rootDealloc -》
object_dispose -》
objc_destructInstance -》
clearDeallocating -》
clearDeallocating_slow

void *objc_destructInstance(id obj) 
{
    if (obj) {
        bool assoc = obj->hasAssociatedObjects();
// 如果有关联对象,就remove掉 
        if (assoc) _object_remove_assocations(obj);

// 弱引用的释放在这里
        obj->clearDeallocating();
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
  //  在这里进行释放
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}


// 找到散列表中的 weak_table 表,找到weak_table 中的 entry,将entry中的 引用对象referrer 置空,最后remove entry
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

总之,释放的时候就是找到散列表中的 weak_table 表,找到weak_table 中的 entry,将entry中的 引用对象referrer 置空,最后remove entry

最后补充一张存储结构图 来源

存储结构图

补充

  • 1、strong
    同样的方法,进入strong底层,可以看到,底层默认实现的是 retain 方法,所以在arc中,strong其实等同于 retrain

  • 2、weak与assign
    assign一般只修饰值类型,虽然也可以修饰引用类型,但是修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃。
    weak 不会产生野指针问题。因为weak修饰的对象释放后(引用计数器值为0),指针会自动被置nil,之后再向该对象发消息也不会崩溃。 weak是安全的。

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