iOS中weak的底层实现原理解析

我们知道weak是弱引用,指向的对象的计数器不会加一,并且在该对象释放的时候会被置为nil。通常用来解决循环引用的问题,但是我们不仅要知其然,还要知其所以然。刚开始写文章,可能思路不是那么清晰,以下是笔者自己的一点粗略看法,如果文章中有什么错误或者不妥,欢迎大家指正。

1. 原理概括

苹果为了管理所有对象的计数器和weak指针,苹果创建了一个全局的哈希表,我们暂且叫它SideTables,里面装的是的名为SideTable的结构体。用对象的地址作为key,可以取出sideTable结构体,这个结构体用来管理引用计数和weak指针。

1.初始化一个weak对象时,runtime会调用一个objc_initWeak函数,初始化一个新的weak指针指向该对象的地址

2.在objc_initWeak函数中会继续调用objc_storeWeak函数,在这个过程是用来更新weak指针的指向,同时创建对应的弱引用表

3.在对象释放时,会调用clearDeallocating函数,这个函数会根据对象地址获取所有weak指针数组,然后遍历这个数组置为nil。最后把该条对象的记录从weak表中删除。

2. 创建weak指针并指向指定对象

1.初始化代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSObject *p = [[NSObject alloc] init];
        __weak NSObject *p1 = p;
    }
    return 0;
}
  1. runtime会在内部调用 objc_initWeak 函数
id objc_initWeak(id *location, id newObj) {
    // 查看对象实例是否有效
    // 无效对象直接导致指针释放
    if (!newObj) {
        *location = nil;
        return nil;
    }
    // 这里传递了三个 bool 数值
    // 使用 template 进行常量参数传递是为了优化性能
    return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
}

可以看到,在这个函数中继续调用了 objc_storeWeak函数。

  1. 在objc_storeWeak函数中更改指针的指向和创建对应的弱引用表,具体实现如下图
template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
    assert(HaveOld  ||  HaveNew);
    if (!HaveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (HaveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (HaveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo<HaveOld, HaveNew>(oldTable, newTable);

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

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (HaveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (HaveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (HaveNew) {
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, 
                                                      (id)newObj, location, 
                                                      CrashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);

    return (id)newObj;
}

从上图中我们可以看到 根据对象的地址,可以取出各自的SideTable结构体实例。前面说过,这个结构体管理者对象的计数器和weak指针。
这里我们先看下几个比较重要的结构体

SideTable

struct SideTable {
    spinlock_t slock; // 因为操作对象的引用计数频率很快,因此系统在这里设置了一把自旋锁,保证是原子操作
    RefcountMap refcnts; // 引用计数器哈希表,根据对象地址查找对象的引用计数
    weak_table_t weak_table; // 维护weak指针的结构体
}

weak_table_t

struct weak_table_t {
    weak_entry_t *weak_entries; // 保存所有指向某一个对象的weak指针的一个数组,循环遍历此数组可找到指定对象的weak_entry_t结构体实例
    size_t    num_entries; // 用来维护数组的size始终在一个合理的大小
    uintptr_t        mask;
    uintptr_t        max_hash_displacement;
};

weak_entry_t

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被指向的对象的地址,前面循环遍历查找的时候就是判断目标地址是否和它相等。
    union {
        struct {
            weak_referrer_t *referrers; // 可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
            uintptr_t        out_of_line : 1;
            uintptr_t        num_refs : PTR_MINUS_1;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // 只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT]; 
        };
    };
};

解除旧对象和该weak指针的绑定

weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
实际中就是遍历该对象的weak_entry_t结构体中的referrers数组,找到并删除这个weak指针

注册新对象和该weak指针的绑定

(objc_object *)weak_register_no_lock(&newTable->weak_table,(id)newObj, location, CrashIfDeallocating);
和解除操作相反,即将该weak指针添加到该对象weak_entry_t结构体中的referrers数组中。

3. 对象的释放过程

对象在释放时,会调用dealloc方法,dealloc方法中也进行了大量逻辑判断和其它处理,这里不展开讨论,最终会调用一个叫做sidetable_clearDeallocating()的函数

{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }

可以看到,如果有弱引用的话,会调用一个weak_clear_no_lock()的函数,这个就是把weak指针置为nil的关键点

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //1、拿到被销毁对象的指针
    objc_object *referent = (objc_object *)referent_id;
 
    //2、通过 指针 在weak_table中查找出对应的entry
    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;
    }
 
    //3、将所有的引用设置成nil
    weak_referrer_t *referrers;
    size_t count;
 
    if (entry->out_of_line()) {
        //3.1、如果弱引用超过4个则将referrers数组内的弱引用都置成nil。
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        //3.2、不超过4个则将inline_referrers数组内的弱引用都置成nil
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
 
    //循环设置所有的引用为nil
    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();
            }
        }
    }
 
    //4、从weak_table中移除entry
    weak_entry_remove(weak_table, entry);
}

整个对象释放过程可以简单概括为:

  1. 根据释放对象的地址,在sideTables哈希表中获取到对应的sideTable结构体中的weak_table结构体实例,将自己和weak_table一起传入到weak_clear_no_lock(&table.weak_table, (id)this)中
  2. 在weak_clear_no_lock()函数中,通过weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);拿到weak_entry_t实例
  3. 遍历weak_entry_t中的weak指针数组,并全部置为nil
  4. 删除weak_table和引用计数表中的对应记录

参考资料:
iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-二
iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析

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