Runtime源代码解读11(内存管理Weak)

1,305 阅读16分钟

2019-12-19

一、概述

Weak 指针是为了解决引用计数内存管理可能存在的循环引用问题而设计的,一种指向对象而不增加对象的引用计数,且指向的对象析构时能自动置nil的特殊指针。

在 NSObject.mm 中找到实现 weak 指针的关键代码template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating> static id storeWeak(id *location, objc_object *newObj),该方法用于将对象赋值给__weak类型引用。

二、源码分析

2.1 准备工作

首先了解几种数据结构,不需完全理解其原理,知道其功能即可。

2.1.1 DisguisedPtr类

DisguisedPtr类的定义如下。它对一个指针伪装处理,保存时装箱,调用时拆箱。可不纠结于为什么要将指针伪装,只需要知道DisguisedPtr<T>功能上等价于T*即可。

template <typename T>
class DisguisedPtr {
    uintptr_t value;

    static uintptr_t disguise(T* ptr) {
        return -(uintptr_t)ptr;
    }

    static T* undisguise(uintptr_t val) {
        return (T*)-val;
    }

 public:
    DisguisedPtr() { }
    DisguisedPtr(T* ptr) 
        : value(disguise(ptr)) { }
    DisguisedPtr(const DisguisedPtr<T>& ptr) 
        : value(ptr.value) { }

    DisguisedPtr<T>& operator = (T* rhs) {
        value = disguise(rhs);
        return *this;
    }
    DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
        value = rhs.value;
        return *this;
    }

    operator T* () const {
        return undisguise(value);
    }
    T* operator -> () const { 
        return undisguise(value);
    }
    T& operator * () const { 
        return *undisguise(value);
    }
    T& operator [] (size_t i) const {
        return undisguise(value)[I];
    }
};

2.1.2 weak_entry_t结构体

首先了解weak_entry_t结构体,代码如下。

#if __LP64__
#define PTR_MINUS_1 63
#else
#define PTR_MINUS_1 31
#endif
#define WEAK_INLINE_COUNT 4

typedef objc_object ** weak_referrer_t;

struct weak_entry_t {
    DisguisedPtr<objc_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 {
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
};

其中weak_referrer_t类型是 对象的引用的地址,是指针的指针。weak_entry_t结构体的结构如下:

  • referent:对象的引用,指向 被弱引用的对象;
  • 联合体保存了 对象(*referent)的所有弱引用的地址。有两种保存形式:
    • inline 形式:out_of_line标记为0,限定referrers长度为WEAK_INLINE_COUNTinline 形式用数组数据结构 保存对象(*referent)的所有弱引用的地址;
    • out of line形式:out_of_line标记为1,__out of line 形式实现了 用哈希表数据结构__保存 对象(*referent)的所有弱引用的地址。referrers指向weak_referrer_t的数组(对象的所有弱引用的地址 的数组),该数组用于保存哈希表的所有元素,num_refs是哈希表中保存的元素个数,占用63位,mask记录哈希表的长度、max_hash_displacement记录哈希表的最大冲突个数,具体使用后文会介绍。

具体分析下weak_entry_t的内存结构。

  • referent是1个指针,占8字节;
  • union联合体为 inline 形式时保存inline_referrers中的4个指针,占32字节;根据结构体内存对齐规则,out of line 形式保存referrersmaskmax_hash_displacement占24字节,num_refsout_of_line共同占用8字节,由于联合体的成员间共享内存空间,因此union联合体固定占用32字节。

inline 和 out_of_line 形式下weak_referrer_t的内存结构 分别如下图所示。

weak_entry_t结构体内存布局.jpg

如图红色标识的指针weak_entry_t* entry指向地址0x100BB13400的内存块,内存块保存weak_entry_t结构体。当weak_entry_t使用 inline 形式保存时,图中绿色标记的0x100BB134010内存块保存的是 对象的弱引用的内存地址;当weak_entry_t使用 inline 形式保存时,图中相同位置的内存块,最低位保存 out_of_line 标记,其余63位保存num_refs。

为什么这块内存的最低位可以标记是否 inline 呢?因为64位机CPU每次访问内存的必定是 以64的倍数为起始地址 读出64位(8字节)数据,为保证CPU读取内存的效率,一个指针的起始地址必然为8的倍数(防止读取一个指针的内容访问两次内存),即64位机指针的地址后3位必定为0。再加之inline_referrers的单元格内容为空时保存0x0,最低位也是0。因此,weak_entry_t每个单元格中的内容的最后一位均为0时,则必定是采用了 inline 形式保存 对象的弱引用的地址

具体内存地址对齐的规则可以看这篇文章:内存地址对齐

2.1.3 weak_table_t结构体

SideTable中包含weak_table_t类型的weak_table成员。weak_table_t结构体是用于保存weak_entry_t哈希表,源代码如下:

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

该结构体中:

  • weak_entries指向weak_entry_t结构体的数组,该数组保存哈希表的所有元素,一个元素对应 一个对象weak_entry_t地址;
  • num_entries是哈希表中保存的weak_entry_t元素的个数;
  • mask记录哈希表的长度、max_hash_displacement记录哈希表的最大冲突个数,具体使用后文会介绍;

2.1.4 总结:

  • 每个被弱引用对象,必然有一个weak_entry_t与之对应,weak_entry_t保存了该对象的地址 以及所有 弱引用了该对象 的引用的地址;

  • SideTableweak_table_t类型的weak_table成员,保存了SideTable管理的对象中 所有被弱引用的对象 的weak_entry_t

2.2 storeWeak 源码解析

实现弱引用的关键代码在template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating> static id storeWeak(id *location, objc_object *newObj)函数中。删除断言、加解锁、等待类初始化等代码,得到简化源码如下。参数含义:

  • HaveOld:是否有旧值;
  • HaveNew:是否指定新值;
  • CrashIfDeallocating:若为true,传入的newObj不支持weak类型或野指针时程序将中断运行;若为false,传入的newObj不支持weak类型或野指针时置新值为nil
  • location:weak指针旧值地址(类型 objc_object **);
  • newObj:weak指针新值(类型 objc_object *);
template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

 retry:
    if (HaveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (HaveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    if (HaveOld  &&  *location != oldObj) {
        goto retry;
    }

    if (HaveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

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

    return (id)newObj;
}

2.2.1 storeWeak 弱引用指向的新对象的处理

首先抽出HaveNewtrue的代码逻辑:

  • 若weak指针指向的新对象newObj,则获取newObjSideTable赋值给newTable
  • 调用 weak_register_no_lock(...)函数 注册新对象;
  • 若新对象newObj非空,调用 setWeaklyReferenced_nolock(...)函数 设置弱引用新对象newObj

注意:之所以先分析新对象处理,是因为这一部分是从无到有的过程,必然包含了很多理解旧对象处理的关键信息。后文阅读代码的顺序都会用到该原则:优先关注从无到有的逻辑

2.2.1.1 weak_register_no_lock 注册新对象

删除不关心代码,weak_register_no_lock(...)函数关键代码如下。其功能是将弱引用对象的地址referent_id以及对象的弱引用的地址referrer_id添加到weak_table中。

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;

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

关注到判断逻辑if ((entry = weak_entry_for_referent(weak_table, referent)))判断逻辑,其含义是从weak_table中查询 新添加对象引用referentweak_entry。先不看weak_entry_for_referent(...)的实现逻辑,初始状态下该判断条件一定为false,所以先看else中的代码。

else块中的处理过程如下:

  • 构建weak_entry_t类型的结构体new_entry
  • new_entry保存 新对象地址;
  • new_entry指定初始使用 inline 形式保存 新对象的弱引用的地址;
  • new_entryinline_referrers[0]保存 新对象的弱引用的地址;
  • new_entryinline_referrers剩下的三个空间nilfor循环);
  • 调用__weak_grow_maybe(weak_table)函数__,在有需要时,扩展weak_table存储空间;
  • weak_entry_insert(weak_table, &new_entry)new_entry添加到weak_table
2.2.1.1.1 weak_grow_maybe 扩展weak_table长度

接下来看看weak_grow_maybe(...)的内部逻辑,代码如下:

#define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0)

static void weak_grow_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    if (weak_table->num_entries >= old_size * 3 / 4) {
        weak_resize(weak_table, old_size ? old_size*2 : 64);
    }
}

其含义是,若weak_tablenum_entries数组的占用率超过3/4,则调用__weak_resize(weak_table_t *weak_table, size_t new_size)函数__ 扩展weak_table_tweak_entries 数组的长度。

至此,可以知道weak_table_t中的mask的用处。mask是一个全1的二进制数,mask + 1weak_table_tweak_entries 数组的长度。每次扩展weak_table_t增加1倍的长度,也就是mask = 2 * mask + 1,至少分配64个指针的长度(64*8字节)。也就是说weak_table_tweak_entries数组长度若不为0,则其长度至少为64。

2.2.1.1.2 weak_resize 重新哈希 weak_table

继续看weak_resize(weak_table_t *weak_table, size_t new_size)的代码:

static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
    size_t old_size = TABLE_SIZE(weak_table);

    weak_entry_t *old_entries = weak_table->weak_entries;
    weak_entry_t *new_entries = (weak_entry_t *)
        calloc(new_size, sizeof(weak_entry_t));

    weak_table->mask = new_size - 1;
    weak_table->weak_entries = new_entries;
    weak_table->max_hash_displacement = 0;
    weak_table->num_entries = 0;  // restored by weak_entry_insert below
    
    if (old_entries) {
        weak_entry_t *entry;
        weak_entry_t *end = old_entries + old_size;
        for (entry = old_entries; entry < end; entry++) {
            if (entry->referent) {
                weak_entry_insert(weak_table, entry);
            }
        }
        free(old_entries);
    }
}

其处理过程如下:

  • 记录weak_table的旧weak_entriesold_entries
  • 分配new_size长度的内存空间new_entries用于保存新的weak_entries
  • weak_table->weak_entries赋值为new_entries
  • weak_tablemax_hash_displacementnum_entries置0,两者会在接下来的调用weak_entry_insert(...)时更新;
  • old_entries有值,则需要调用 weak_entry_insert(...)函数 将其中的所有weak_entry_t重新哈希保存到new_entries中(for循环),完成重新哈希释放old_entries占用的内存(free(old_entries));
2.2.1.1.3 weak_entry_insert 元素插入 weak_table

继续看weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)实现weak_table的单个weak_entry_t重新哈希的代码:

static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
    weak_entry_t *weak_entries = weak_table->weak_entries;
    assert(weak_entries != nil);

    size_t index = hash_pointer(new_entry->referent) & (weak_table->mask);
    size_t hash_displacement = 0;
    while (weak_entries[index].referent != nil) {
        index = (index+1) & weak_table->mask;
        hash_displacement++;
    }

    weak_entries[index] = *new_entry;
    weak_table->num_entries++;

    if (hash_displacement > weak_table->max_hash_displacement) {
        weak_table->max_hash_displacement = hash_displacement;
    }
}

理解上述代码的关键在于理解这两行代码:

  • size_t index = hash_pointer(new_entry->referent) & (weak_table->mask):其中,hash_pointer(...)用于计算指针的哈希值(代码在下方,不过实现细节可以不纠结),之所以哈希的是new_entry->referent(对象的地址)是因为 区分weak_table->weak_entries中的weak_entry_t的标准是 weak_entry_t所保存的对象。

  • while (weak_entries[index].referent != nil):以线性探测法解决哈希冲突,若weak_entries内 哈希计算的索引 已被占用,则递增索引同时递增hash_displacement计数进入下一次循环迭代。这里之所以用index = (index+1) & weak_table->mask是为了能在探测索引到达weak_table的最大索引时归0;

  • weak_table->max_hash_displacement = hash_displacementmax_hash_displacement是用于记录weak_table的最大冲突数量。在使用对象地址 在weak_table->weak_entries中哈希搜索weak_entry_t时,迭代次数超过max_hash_displacement可直接返回nil。其作用是尽量压低哈希搜索的迭代次数;

对哈希函数有兴趣的话其代码如下:

static inline uintptr_t hash_pointer(objc_object *key) {
    return ptr_hash((uintptr_t)key);
}
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
    key ^= key >> 4;
    key *= 0x8a970be7488fda55;
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key)
{
    key ^= key >> 4;
    key *= 0x5052acdb;
    key ^= __builtin_bswap32(key);
    return key;
}
#endif
2.2.1.1.4 weak_entry_for_referent 获取对象的 weak_entry

分析完weak_register_no_lock(...)if ((entry = weak_entry_for_referent(weak_table, referent)))判断逻辑的else分支,再回头看weak_entry_for_referent(..)以及if分支中的append_referrer(weak_entry_t *entry, objc_object **new_referrer),虽然代码稍长但也会稍微不那么吃力。

weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)函数代码如下。功能是使用referent(对象的地址)计算哈希索引,使用线性探测法迭代搜索weak_table->weak_entries,迭代次数不超过weak_table->max_hash_displacement

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];
}
2.2.1.1.5 append_referrer 添加对象的弱引用地址

当在weak_table中哈希搜索到对象referent对应的weak_entry_t类型的entry时,调用append_referrer(entry, referrer)函数将 对象的弱引用的地址referrer 添加到entry中。代码如下,这里将处理过程通过注释加到代码中:

static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    // 1、若entry使用inline形式保存对象的弱引用的地址new_referrer
    if (! entry->out_of_line) {
        // 2、在inline_referrers中搜索空单元,若存在则填入new_referrer并返回
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        // 3、若inline_referrers没有空单元,则分配4个单元(32字节)长度的new_entry
        // 用于保存inline_referrers中的弱引用地址数组,并赋值给entry->referrers,将
        // entry的保存形式指定为out of line,mask的指定参照weak_table的mask,并初始化
        // max_hash_displacement为0
        // 
        // 注意:这里没有对new_referrers哈希处理,是因为entry的占用度为100%必然会触发
        // 后面的重新哈希。
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));

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

    // 4、若entry的referrer的占用率超过3/4则触发重新哈希,重新哈希过程参考weak_table
    // grow_refs_and_insert函数跟前面weak_grow_maybe差不多,后面会贴出代码不做赘述。
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }

    // 5、使用new_referrer计算哈希值,使用线性探测法将new_referrer添加到entry->referrers中,
    // 处理跟前面weak_entry_insert差不多,不做赘述。w_hash_pointer等价于前面的hash_pointer
    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++;
}

__attribute__((noinline, used))
static void grow_refs_and_insert(weak_entry_t *entry, 
                                 objc_object **new_referrer)
{
    assert(entry->out_of_line);

    size_t old_size = TABLE_SIZE(entry);
    size_t new_size = old_size ? old_size * 2 : 8;

    size_t num_refs = entry->num_refs;
    weak_referrer_t *old_refs = entry->referrers;
    entry->mask = new_size - 1;
    
    entry->referrers = (weak_referrer_t *)
        calloc(TABLE_SIZE(entry), sizeof(weak_referrer_t));
    entry->num_refs = 0;
    entry->max_hash_displacement = 0;
    
    for (size_t i = 0; i < old_size && num_refs > 0; i++) {
        if (old_refs[i] != nil) {
            append_referrer(entry, old_refs[I]);
            num_refs--;
        }
    }
    // Insert
    append_referrer(entry, new_referrer);
    if (old_refs) free(old_refs);
}
2.2.1.2 setWeaklyReferenced_nolock 设置弱引用新对象

完成弱引用注册后,新对象newObj需要调用setWeaklyReferenced_nolock(...)方法标记对象被弱引用。因为对象在析构时,若对象被弱引用,则需要将这些弱引用全部置nil。代码如下:

inline void
objc_object::setWeaklyReferenced_nolock()
{
 retry:
    isa_t oldisa = LoadExclusive(&isa.bits);
    isa_t newisa = oldisa;
    if (!newisa.indexed) return sidetable_setWeaklyReferenced_nolock();
    if (newisa.weakly_referenced) return;
    newisa.weakly_referenced = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}

这里,在从Runtime源代码解读内存管理机制——Retain/Release中介绍的isa.bits再次粉墨登场。处理逻辑如下:

  • isa.bitsindexed位为1(只使用isa 或使用isa联合SideTable保存对象引用计数),则设置isa.bits的弱引用标记位为1;
  • isa.bitsindexed位为0(只使用SideTable保存对象引用计数),则调用sidetable_setWeaklyReferenced_nolock()方法,其源代码如下,只是简单设置了SideTablerefcnts中该对象的引用计数信息的弱引用标记位为1,代码如下:
void 
objc_object::sidetable_setWeaklyReferenced_nolock()
{
    SideTable& table = SideTables()[this];

    table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
}
2.2.1.3 总结storeWeak对新对象的处理

storeWeak(...)函数对新对象的核心处理流程如下:

  • 根据新对象newObj定位到目标SideTable,从中取出weak_table
  • 判断weak_table的使用率是否超过3/4,超过则对其扩容并重新哈希;
  • weak_tableweak_entries中,通过newObj哈希,使用线性探测法迭代搜索newObj对应的weak_entry
  • 若搜索不到,则在weak_tableweak_entries中,通过newObj哈希,使用线性探测法添加newObjweak_entry并指定使用inline形式保存referrers
  • 至此,得到新对象的weak_entry
  • weak_entryreferrers使用inline形式保存,则在inline_referrer中的空位填入newObj的弱引用的地址newReferrer,跳过下一步;若无空位,则将referrers保存形式转换为out of line,并将inline_referrer中的数据转移到referrers
  • weak_entryreferrers使用out of line形式保存,则判断referrers的使用率是否超过3/4,超过则对其扩容并重新哈希。在referrers中,通过newReferrer哈希,使用线性探测法添加newReferrer
  • 至此,新对象的weak_entry添加到weak_table中;
  • 标记新对象被弱引用。若isa.indexed为0,则找到对象在SideTable中记录的引用计数信息,将弱引用标记置为1;若isa.indexed为1,则将新对象的isa.bits的弱引用标记置为1。

重点一weak_entry_t实现了保存 一个对象的所有weak_referrer_t(对象的所有弱引用的地址)的哈希表,使用weak_referrer_t计算哈希索引,哈希表长度可动态扩展使用线性探测法解决冲突。

重点二weak_table_t实现了保存一组对象的weak_entry_t的哈希表,使用对象地址referrer计算哈希索引,哈希表长度可动态扩展使用线性探测法解决冲突。

2.2.2 storeWeak 弱引用指向的旧对象的处理

上一节介绍了storeWeak(...)处理新对象的过程,本节接着介绍storeWeak(...)处理旧对象的过程。先从中抽出HaveOldtrue的代码逻辑:

  • 若弱引用指向的旧对象oldObj非空,则获取oldObjSideTable赋值给oldTable
  • 调用weak_unregister_no_lock(...)函数;

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

有前面打下的基础,上面的代码逻辑已经比较明了:

  • 若旧对象referent为空则直接返回;
  • weak_table中存在旧对象referent对应的entry:则调用remove_referer(entry, referrer)entry中删除该弱引用的地址referrer;再判断entry是否为空,若使用out of line形式保存referrers则只要查询num_refs是否为0,若使用inline则需要遍历inline_referrersentry为空时调用weak_entry_remove(weak_table, entry)entryweak_table中删除;

继续看remove_referrer(weak_entry_t *entry, objc_object **old_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_remove(weak_table_t *weak_table, weak_entry_t *entry)的代码,处理过程写在代码注释中:

static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    if (entry->out_of_line) free(entry->referrers);
    bzero(entry, sizeof(*entry));

    weak_table->num_entries--;

    weak_compact_maybe(weak_table);
}
static void weak_compact_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    // Shrink if larger than 1024 buckets and at most 1/16 full.
    if (old_size >= 1024  && old_size / 16 >= weak_table->num_entries) {
        weak_resize(weak_table, old_size / 8);
        // leaves new table no more than 1/2 full
    }
}

至此,总结一下storeWeak对旧对象的核心处理流程:

  • 根据旧对象oldObj定位到目标SideTable,从中取出weak_table
  • 若根据旧对象找到weak_tableoldObjweak_entry
  • weak_entry保存referrers的形式为inline,则遍历inline_referrers删除当前弱引用的地址;若weak_entry保存referrers的形式为out of line,则使用当前弱引用的地址计算哈希值,用线性探测法搜索referrers,从中删除当前弱引用的地址;
  • 至此已从weak_entryreferrers删除当前弱引用的地址;
  • 若移除当前指针后,referrers中的referrer数量为0,则将旧对象对应entryweak_tableweak_entries中移除,移除同样通过oldObj的引用计算哈希值,使用线性探测法搜索;
  • weak_table移除weak_entries后,若weak_entries的长度超过1024且weak_entries的占用率不超过1/16,则将weak_entries的长度缩短为原1/8并重新哈希weak_entries

三、总结

最后用下图表示SideTableweak_table_tweak_entry_tweak_referent_tweak_referrer_t之间的关系。图比较大,看不清可以下载原图查看。注意:

  • 锐利箭头表示指向关系(左侧表示保存内存地址,右侧表示地址中所存储的内容);
  • 初始状态下内存中存在两个对象 oldObj 和 newObj,假设两个对象的引用计数均保存在同一张SideTable中,用 inline 形式保存 oldObj 的所有弱引用的地址,用 out of line 形式保存 newObj 的所有弱引用的地址;
  • 弱引用 weakRef 指向 oldObj;
  • 图中左侧的 oldObj 和右侧的 oldObj 是同一个对象,左侧的 weakRef 和 右侧的 weakRef 是同一个对象引用,均指向 oldObj,左侧的weakReferrer 和右侧的 weakReferrer 均是 weakRef 的地址;
  • weak_entries哈希表动态调整容量、refferrers哈希表动态调整容量、记录对象是否被弱引用的过程不作图示。

初始关系图

接下来调用 storeWeak(weakReferrer, newObj) 让 weakRef 指向 newObj,首先 storeWeak 处理旧对象的示意图。黄色平面箭头表示操作流程,黄色标识块表示流程涉及的数据。处理完成后结果如左上角所示,weakRef 指向nil

storeWeak处理旧对象.jpg

最后是storeWeak处理新对象的示意图。红色平面箭头表示操作流程,红色标识块表示流程涉及的数据。处理完成后结果如左上角所示,weakRef 指向newObj。

storeWeak处理新对象.jpg