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
        (location, (objc_object*)newObj);
}

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

  1. 在objc_storeWeak函数中更改指针的指向和创建对应的弱引用表,具体实现如下图
template 
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(oldTable, newTable);

    if (HaveOld  &&  *location != oldObj) {
        SideTable::unlockTwo(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(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(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 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对象的初始化,引用,释放的分析

你可能感兴趣的:(iOS中weak的底层实现原理解析)