细说weak

作者: 塔米尔 | 来源:发表于2018-08-04 18:40 被阅读330次
  • 被weak修饰的对象在被释放的时候会发生什么?
  • 是如何实现的?
  • 知道sideTable么?
  • 里面的结构可以画出来么?

在回答这些问题之前,我们先来了解一下weak的内部结构。

对于我们常说的weak,其实是一个由Runtime维护的用于存储对象的所有weak指针的hash表。key是所指对象的指针,value是weak指针的地址(这个地址的值是所指对象的地址)数组。 光这么说还是不好理解,来看一个列子吧

NSObject *b = [NSObject new];
__weak id a = b;

这里的b就是weak表的key,&a(a的内存地址)就是value;

那这个结论是怎样得到的?

在我们加入runtime源码进行调试的时候,我们发现 weak 修饰符变量是通过 objc_initWeak 函数来初始化的,在变量作用域结束的时候通过 objc_destroyWeak 函数来释放该变量的。这两个函数长下面这样👇

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);
}
void objc_destroyWeak(id *location)
{
    (void)storeWeak<true/*old*/, false/*new*/, false/*crash*/>
        (location, nil);
}

从上两段代码不难发现它们都调用了storeWeak 这个函数,但是两个方法传入的参数却稍有不同。
init 方法中,第一个参数为 weak 修饰的变量,第二个参数为引用计数对象。但在 destoryWeak 函数,第一参数依旧为 weak 修饰的变量,第二个参数为 nil。那这块传入不同的参数到底代表什么,我们继续分析 storeWeak 这个函数。

// 更新一个弱引用变量
// 如果 HaveOld 是 true, 变量是个有效值,需要被及时清理。变量可以为 nil。
// 如果 HaveNew 是 true, 需要一个新的 value 来替换变量。变量可以为 nil
// 如果crashifdeallocation 是 ture ,那么如果 newObj 是 deallocating,或者 newObj 的类不支持弱引用,则该进程就会停止。
// 如果crashifdeallocation 是 false,那么 nil 会被存储。

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) {
    // 如果 HaveOld 为 true ,更改指针,获得以 oldObj 为索引所存储的值地址
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (HaveNew) {
    // 获得以 newObj 为索引所存储的值对象
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    
// 对两个 table 进行加锁操作,防止多线程中竞争冲突
    SideTable::lockTwo<HaveOld, HaveNew>(oldTable, newTable);

//  location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改, 保证线程安全,这个判断用来避免线程冲突重处理问题
    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.
    // 防止弱引用之间发生死锁,并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向
    if (HaveNew  &&  newObj) {
        // 获得新对象的 isa 指针
        Class cls = newObj->getIsa();
        // 判断 isa 非空且已经初始化
        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.
            // 如果该类已经完成执行 +initialize 方法是最好的,如果该类 + initialize 在线程中,例如 +initialize 正在调用storeWeak 方法,那么则需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记然后重新尝试
            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
        // 如果弱引用被释放则该方法返回 nil
        // 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;
}

以上就是 store_weak 这个函数的实现,它主要做了以下几件事:

  • 声明了新旧散列表指针,因为 weak 修饰的变量如果之前已经指向一个对象,然后其再次改变指向另一个对象,那么按理来说我们需要从oldTable中删除 weak 变量的记录,也就是要释放该weak变量,然后再给newTable添加新记录(weak变量)。这里的新旧散列表就是这个作用。
  • 根据新旧变量的地址获取相应的 SideTable
  • 对两个表进行加锁操作,防止多线程竞争冲突
  • 进行线程冲突重处理判断
  • 判断其 isa 是否为空,为空则需要进行初始化
  • 如果存在旧值,调用 weak_unregister_no_lock 函数清除旧值
  • 调用 weak_register_no_lock 函数分配新值
  • 解锁两个表,并返回第二参数

tip

你可以把objc_storeWeak(id *location, objc_object *newObj)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。
结合最开始的例子我们可以理解为objc_storeWeak(&a, b)
在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。
而如果a是由 assign 修饰的,则: 在 b 非 nil 时,a 和 b 指向同一个内存地址,在 b 变 nil 时,a 还是指向该内存地址,变野指针。此时向 a 发送消息极易崩溃。

初始化弱引用对象流程一览

初始化弱引用对象流程一览

SideTable

可能大家对sideTale不太明白,sideTable主要用于管理对象的引用计数和 weak 表。在 NSObject.mm 中声明其数据结构:

struct SideTable {
// 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

对于 slock 和 refcnts 两个成员不用多说,第一个是为了防止竞争选择的自旋锁,第二个是协助对象的 isa 指针的 extra_rc 共同引用计数的变量。这里主要看 weak_table_t 的结构与作用。

weak_table_t

weak_table_t结构体存储了某个对象相关的的所有的弱引用信息。其定义如下(具体定义在objc-weak.h中):

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

使用weak 指针指向的对象的地址作为 key ,用 weak_entry_t 类型结构体对象作为 value 。其中的 weak_entries 成员,从字面意思上看,即为弱引用表入口。

weak_entry_t

其中weak_entry_t是存储在 weak_table_t 中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。其定义如下:

typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
    DisguisedPtrobjc_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];
        };
    }
}

referent:
被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。

referrers:
可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。

inline_referrers:
只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。

讲了这3个东西,我们用一张图来体现他们的关系

weak_unregister_no_lock

结合刚刚讲到的从oldTable中删除 weak 变量的记录,来看看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;

    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        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;
                }
            }
        }

        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }

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

该方法主要作用是将旧对象在 weak_table 中解除 weak 指针的对应绑定。根据函数名,称之为解除注册操作。
来看看这个函数的逻辑。首先参数是 weak_table_t 表,键和值。声明 weak_entry_t 变量,如果key(referent)为空,直接返回。根据全局入口表和键获取对应的 weak_entry_t 对象 entry。获取到entry后,将entry以及 weak_table作为参数传入 remove_referrer 函数中,这个函数就是解除操作。然后判断entry是否为空,如果为空,从全局记录表中清除相应的entry。

weak_entry_for_referent

接下来,我们了解一下,如何获取这个 weak_entry_t 这个变量。

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 表入口,然后将引用计数对象的地址进行 hash 化后与 weak_table->mask 做与操作,作为下标,在全局 weak 表中查找,若找到,返回entry,若没有,返回nil。

remove_referrer

再来了解一下解除对象的函数:

static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
    if (! entry->out_of_line) {
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == old_referrer) {
                entry->inline_referrers[i] = nil;
                return;
            }
        }
        _objc_inform("Attempted to unregister unknown __weak variable "
                     "at %p. This is probably incorrect use of "
                     "objc_storeWeak() and objc_loadWeak(). "
                     "Break on objc_weak_error to debug.\n", 
                     old_referrer);
        objc_weak_error();
        return;
    }

    size_t index = w_hash_pointer(old_referrer) & (entry->mask);
    size_t hash_displacement = 0;
    while (entry->referrers[index] != old_referrer) {
        index = (index+1) & entry->mask;
        hash_displacement++;
        if (hash_displacement > entry->max_hash_displacement) {
            _objc_inform("Attempted to unregister unknown __weak variable "
                         "at %p. This is probably incorrect use of "
                         "objc_storeWeak() and objc_loadWeak(). "
                         "Break on objc_weak_error to debug.\n", 
                         old_referrer);
            objc_weak_error();
            return;
        }
    }
    entry->referrers[index] = nil;
    entry->num_refs--;
}

这个函数传入的是 weak_entry 对象,当 out_of_line 为0 时,遍历数组,找到对应的对象,置nil,如果未找到,报错并返回。当 out_of_line 不为0时,根据对象的地址 hash 化并和 mask 做与操作作为下标,查找相应的对象,若没有,报错并返回,若有,相应的置为 nil,并减少元素数量,即 num_refs 减 1。

清除旧值就讲完了
来看看添加新值

weak_register_no_lock

id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }

    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } 
    else {
        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);
    }

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

    return referent_id;
}

一大堆 if-else, 主要是为了判断该对象是不是 taggedPoint 以及是否正在调用 dealloca 等。下面操作开始,同样是先获取 weak_entry,如果获取到,则调用 append_referrer 插入对象,若没有,则新建一个 weak_entry,默认为 out_of_line,然后将新对象放到 0 下标位置,其他位置置为 nil 。下面两个函数 weak_grow_maybe 是用来判断是否需要重申请内存重 hash,weak_entry_insert 函数是用来将新建的 weak_entry 插入到全局 weak 表中。插入时同样是以对象地址的 hash 化和 mask 值相与作为下标来记录的。

接下来看看 append_referrer 函数,源代码如下:

append_referrer
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if (! entry->out_of_line) {
        // Try to insert inline.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        // Couldn't insert inline. Allocate out of line.
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        // This constructed table is invalid, but grow_refs_and_insert
        // will fix it and rehash it.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[I];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line = 1;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    assert(entry->out_of_line);

    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    size_t index = w_hash_pointer(new_referrer) & (entry->mask);
    size_t hash_displacement = 0;
    while (entry->referrers[index] != NULL) {
        index = (index+1) & entry->mask;
        hash_displacement++;
    }
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

当 out_of_line 为 0,并且静态数组里面还有位置存放,那么直接存放并返回。如果没有位置存放,则升级为动态数组,并加入。如果 out_of_line 不为 0,先判断是否需要扩容,然后同样的,使用对象地址的 hash 化和 mask 做与操作作为下标,找到相应的位置并插入。

对象的销毁以及 weak 的置 nil 实现

释放时,调用clearDeallocating函数。clearDeallocating 函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

  • 调用 objc_release
  • 因为对象的引用计数为0,所以执行dealloc
  • 在dealloc 中,调用了_objc_rootDealloc 函数
  • 在 _objc_rootDealloc 中,调用了 objec_dispose 函数
  • 调用objc_destructInstance
  • 最后调用 objc_clear_deallocating

objc_clear_deallocating的具体实现如下:

void objc_clear_deallocating(id obj) 
{
    assert(obj);
    assert(!UseGC);

    if (obj->isTaggedPointer()) return;
    obj->clearDeallocating();
}

这个函数只是做一些判断以及更深层次的函数调用,

void objc_object::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);
    }
    table.unlock();
}

我们可以看到,在这个函数中,首先取出对象对应的SideTable实例,如果这个对象有关联的弱引用,则调用weak_clear_no_lock来清除对象的弱引用信息,我们在来深入一下

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);
}

这个函数根据 out_of_line 的值,取得对应的记录表,然后根据引用计数对象,将相应的 weak 对象置 nil。最后清除相应的记录表。

通过上面的描述,我们基本能了解一个weak引用从生到死的过程。从这个流程可以看出,一个weak引用的处理涉及各种查表、添加与删除操作,还是有一定消耗的。所以如果大量使用__weak变量的话,会对性能造成一定的影响。那么,我们应该在什么时候去使用weak呢?《Objective-C高级编程》给我们的建议是只在避免循环引用的时候使用__weak修饰符。

好了本文开篇提出的几个问题的答案都能在上面找到。

参看
iOS 中weak的实现
iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-一
细说weak
runtime 如何实现 weak 属性
iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)

相关文章

  • 细说weak

    被weak修饰的对象在被释放的时候会发生什么? 是如何实现的? 知道sideTable么? 里面的结构可以画出来么...

  • Block循环引用

    __weak __weak LDPerson *weakPerson = person__weak typeof(...

  • weak、strong、@ property

    面试题:weak实现原理? 一、weak weak 基本用法 weak 是弱引用,用 weak 来修饰、描述所引用...

  • 解读objc源码:weak的实现原理

    一、weak的作用 这里的weak包括属性关键字weak和__weak两种:__weak用于修饰变量(variab...

  • iOS 弱引用weak与weak_table

    Weak与Weak表 The weak table is a hash table governed by a s...

  • 面试

    1. assign vs weak, __block vs __weak strong 和 weak 是在arc后...

  • 2020-09-28 技术回顾

    weak的实现原理 Runtime 维护了一个 weak表,用于存储指向某个对象的所有weak指针。weak表 其...

  • dailyLearning -- 关键字weak实现原理

    weak 实现原理的概括 Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其...

  • #2-weak 实现原理

    weak 实现原理的概括 Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其...

  • iOS-weak

    weak实现原理: Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。 weak表其实是...

网友评论

    本文标题:细说weak

    本文链接:https://www.haomeiwen.com/subject/qcwmvftx.html