iOS weak弱引用的底层实现

485 阅读11分钟

源码版本:objc4-779.1 当然还是推荐使用这个来学习:可编译的源码

weak是一个所有权修饰符, 它提供弱引用的功能, 即弱引用者(weak 修饰的变量, 后统称为弱引用者)不能持有引用对象, 当引用对象被释放时, 此弱引用者被置为 nil. 此文将探究弱引用在底层是如何实现

弱引用者如何注册到弱引用表

首先我们研究弱引用者是如何注册到弱引用表中的 调试代码如下(代码需要在开头提到的可编译版本里面才能运行, 后面不再解释), 注意里面打了个断点, 运行程序

点击 step into, 会跳转到函数objc_initWeak()

这个函数的作用是初始化弱引用者. 需要注意的是storeWeak模板里面的两个参数 DontHaveOld, DoHaveNew. 它们分别是HaveOldHaveNew枚举的变量. 枚举里面变量的含义如下

  • DontHaveOld: 表示弱引用者之前没有引用对象, 例如在用__weak id weakPtr = [[NSObject alloc] init];初始化弱引用者

  • DoHaveOld: 表示弱引用者之前有引用对象. 此时需要将弱引用者从弱引用表中注销

  • DontHaveNew: 表示弱引用者没有引用一个新的对象

  • DoHaveNew: 表示弱引用者引用了一个新的对象. 此时需要将弱引用者注册到弱引用表中

继续 step into, 跳转到storeWeak()函数里面

函数里面代码比较多, 为了方便, 我把它分成了几部分来说明

SideTable跟弱引用的实现息息相关, 在这里可以看到对这个类的详细说明. 这里我就简单的介绍一下, 它是存放引用计数表和弱引用表的结构. 它有三个成员变量:引用计数表, 弱引用表, 自旋锁. 在我们操作引用计数表或者弱引用表的时候自旋锁会加锁防止竞态条件的出现.

代码块 1 的作用:

  • 加锁, 防止在修改哈希表时可能出现的竞态条件. 按锁地址(从低到高)顺序加锁, 防止可能出现的锁的排序问题.
  • 如果弱引用者的引用对象改变了(这是因为如果并发修改弱引用者的引用对象, 在加锁之前, 可能原先的引用对象会改变), 则重复执行retry部分的代码

代码块 2 的作:

  • 保证引用对象的 isa 指针已经初始化过. 如果未初始化, 则对该对象的非元类 Class 进行初始化.

代码块 3 的作:

  • 在弱引用表上注销弱引用者, 后面会再来讲这个部分

代码块 4 的作用:

  • 在弱引用表上注册弱引用者

代码块 5 的作用:

  • 对象被弱引用者引用了, 则修改对象 isa 指针上位域的信息, 将weakly_referenced置为 YES, 表示该对象被一个弱引用者引用.

需要注意的是, 如果这个对象后续不再被弱引用者引用了, isa 指针上的weakly_referenced值仍旧是 YES

接着, 我们把断点移动到weak_register_no_lock()函数里面, 同样, 我把它分成几部分来讲解.

referent 表示引用对象, referrer 表示弱引用者

代码块 1 的作用是:

  • 保证引用对象是可用的. 如果引用对象正处于被销毁的状态, 那么程序会崩溃

代码块 2 的作用是:

  • 根据引用对象找到对应的 weak_entry_t 实例(后面统称为为弱条目), 并将该弱引用者保存到弱条目中

下面是append_referrer()函数的实现, 写了点注释就不展开讲了

static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if (! entry->out_of_line()) {
    	 // 使用内部数组来保存弱引用者. 遍历内部数组, 如果有空位则用该空位保存弱引用者并返回
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

		 // 如果执行到这里, 说明内部数组都被使用了. 所以需要扩容, 即创建一个外部数组来保存弱引用者
		 // 先初始化一个跟内部数组长度一样的数组, 并将内部数组的数据转移过去
        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_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    ASSERT(entry->out_of_line());
    // 如果外部数组中有 3/4 的元素都被使用了, 则进行扩容
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        // 扩容的长度为原来的 2 倍, 最小值为 8.
        return grow_refs_and_insert(entry, new_referrer);
    }
    // 根据指针的哈希值来计算位置 index
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != nil) {
        // 发生了哈希碰撞, 则增加下标, 查看下一个位置是否为空
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    // 重新计算 entry 的最大哈希偏移量
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    // 将新的弱引用者保存到弱引用条目中
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    // num_refs + 1
    entry->num_refs++;
}

代码块 3 的作用:

生成一个新的弱引用条目, 将弱引用者保存到弱条目中. 随后将弱条目插入到弱引用表中

  • 第一行代码的作用是初始化一个weak_entry_t的实例 new_entry
  • 第二行代码里面函数的作用是, 如果弱引用表的成员变量weak_entries数组里面有 3/4 的数量的弱条目被使用了, 则进行扩容. 长度为原来的 2 倍, 最小为 64
  • 第三行代码的作用是将新生成的弱条目插入到弱引用表中

至此, 我们已经大致了解了弱引用者是如何注册到弱引用条目的, 这里小总结一下:

  • 首先判断引用对象是否处于销毁状态, 否则是的话程序会崩溃
  • 根据引用对象获取到对应的SideTable, 它里面有成员变量弱引用表weak_table_t
  • 在弱引用表中根据引用对象来查找对应的弱引用条目, 如果存在则将该弱引用者保存在该条目中. 如果不存在则新生成一个条目, 将弱引用者保存到该条目, 随后将条目插入到弱引用表中
  • 在弱引用表中插入弱条目根据引用对象的哈希值来计算 index, 在弱条目中插入弱引用者根据弱引用者的哈希值来计算 index
  • 在此过程中, 不对任何的弱引用者进行赋值, 即不对 *referrer 进行赋值操作

弱引用者如何从弱引用表中注销

这部分用来探究弱引用者如何从弱引用表中注销 实验代码如下

点击step into跳转到objc_storeWeak()函数里面

首先弱引用者 w1 引用了对象 o1, w1 会被注册到 o1 对应的弱引用条目中. 随后我们给它设置了一个新的引用对象 o2, 那么, w1 会先从 o1 的条目中注销, 然后注册到 o2 的条目中.

继续调试, 将断点移动到storeWeak()函数里面

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating 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(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;
}

函数里面大部分内容已经在上一节中介绍过, 这里就不重复了 这里我们要讲解的是weak_unregister_no_lock()函数. 这个函数的作用是将销弱引用者从引用对象对应的弱引用条目中注销. 函数实现如下:

referent表示原先的引用对象, referrer表示弱引用者

代码块 1 的作用是根据引用对象查找弱引用条目, 然后存在则将该弱引用者从条目中注销

函数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;
    }
    // 此时使用外部数组来保存弱引用者
    // 根据弱引用者的哈希值来得到对应元素的 index 
    size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != old_referrer) {
        // 如果发生了哈希碰撞, 则偏移+1 往下继续查找该弱引用者.当偏移量超过最大偏移量时程序崩溃
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
        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--;
}

代码块 2 的作用是判断弱引用条目中注册的弱引用者的数量是否为 0, 如果为 0 则将该弱引用条目从弱引用表中清除.

weak_entry_remove()函数的实现如下, 写了点注释就不展开讲了

static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    // remove entry
    // 如果使用的外部数组, 则将外部数组销毁. 如果使用内部数组, 则会在销毁条目时将内部数组也销毁
    if (entry->out_of_line()) free(entry->referrers);
    // 销毁弱引用条目
    bzero(entry, sizeof(*entry));

    weak_table->num_entries--;
	 // 当条目数量超过 1024, 并且使用了不到 1/16, 则将条目数量缩小为原来的 1/8
    weak_compact_maybe(weak_table);
}

至此, 我们已经大致了解了弱引用者如何从弱引用表中注销的, 我小总结一下:

  • 根据旧引用对象获取SideTable
  • 根据就引用对象在SideTable的弱引用表中获取对应的弱引用条目
  • 在弱引用条目中查找是否注册了该弱引用者, 如果存在的话则移除
  • 若弱引用者移除后弱引用表中弱引用者的数量为 0, 则将该条目从弱引用表中移除
  • 如条目移除后, 条目的数量超过 1024, 且使用的数量少于 1/16, 则将条目的容量缩小为原先的 1/8

引用对象销毁后弱引用者如何置为 nil

我们都知道当引用对象被销毁后, 弱引用者会被置为 nil. 这部分用来粗略的讲解其实现

添加一个symbolic breakpoint, 在symbol里面输入[NSObject dealloc]

点击 continue program execution, 跳转到dealloc()函数中

对 o1 的引用计数 -1 , o1 的引用计数就变成了 0, 需要销毁. 即调用 NSObject 的 dealloc 方法

随后一直跳转到objc_object::clearDeallocating_slow()函数里面. 这个函数的作用是清除该对象在SideTable中存在于引用计数表或者弱引用表中的数据

因为研究的是对弱引用的操作, 所以这边我们只需要关注weak_clear_no_lock()函数. 它的实现如下, 写了注释就不展开讲了

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    // referent 表示引用对象, 即这里要被销毁的对象
    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) {
            // 将弱引用者置为 nil
            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();
            }
        }
    }
    // 将所有弱引用者置为 nil 后, 将该条目从弱引用表中删除
    weak_entry_remove(weak_table, entry);
}

至此, 我们已经大致了解了引用对象销毁后弱引用者如何置为 nil 的, 这里小总结一下:

当对象的引用计数为 0 时, 会调用 dealloc 方法用来销毁对象. 如果有弱引用者引用者该对象, 那么会从弱引用表中找到对象的弱引用条目, 将条目中所有注册的弱引用者置为 nil, 随后将该条目从弱引用表中删除.