iOS Runtime随笔——weak原理探究

一、引言

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.
@property (nonatomic, weak) TestObject *weakObj;

// 2.
__weak TestObject *weakObj = obj;

// 3.
__weak __typeof__(self) weakSelf = self;
TestBlock block = ^{
__strong __typeof__(weakSelf) strongSelf = weakSelf;
// do something inside the block
...
}

相信大家对上述代码不陌生,weak修饰对象表示对该对象的弱引用,弱引用不会增加对象的引用计数,并且在所指向的对象被释放之后,weak指针会被设置的为nil,同时避免了”野指针(或者suspend pointer)”的问题。在使用代理模式或者block时,为了避免引起retain cycle而导致内存泄露,我们通常使用weak关键字来打破循环引用链。

之前只是单纯的知道使用weak,但是对于更深层的原理,正文开始前,先提出三个问题:

  1. weak为什么不会增加对象的引用计数
  2. weak对象存储在哪里
  3. 对象释放时其weak指针如何自动设置为nil

现在带着这些问题来一探究竟。本系列文章使用的源码:objc4-750

二、存储结构分析

先看下测试代码:

1
2
NSObject *obj = [NSObject new];
__weak NSObject *weakObj = obj;

__weak出断点并单步运行,发现它实际上调用了objc_initWeak函数。

在runtime源码中找到相关函数的实现如下:

1
2
3
4
5
6
7
8
9
10
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

该函数有两个入参:

  • location: __weak指针的地址
  • newObj: 需要进行弱引用的对象的指针

实际上该函数只是一个更深层函数的入口,在调用更深层函数之前做了一些判断,如果newObj已经被释放,同时置weak对象指针为空,并返回nil。否则调用storeWeak函数进行下一步处理。

注意,从函数的注释中可以看出,objc_initWeak不是线程安全的,因此在设置或修改weak对象时,注意避免一些因多线程引发的问题。

2.1 objc_storeWeak函数

先看下函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
// 断言:haveOld和haveNew必然有一个为true
assert(haveOld || haveNew);
// 如果haveNew为false, 断言newObj为nil
if (!haveNew) assert(newObj == nil);

// 初始化一个Class类型指针previouslyInitializedClass
Class previouslyInitializedClass = nil;
id oldObj;
// 声明两个SideTable散列表
SideTable *oldTable;
SideTable *newTable;

retry:
if (haveOld) {
// 如果有旧值,从SideTables中获取以oldObj为索引的值并赋值给oldTable
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
// 如果有新值,从SideTables中获取以newObj为索引的值并赋值给newTable
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}

// 对oldTable和newTable加锁,防止多线程读写竞争
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

// 如果有旧值,*location应该与oldObj相同,
// 否则说明当前oldObj已经被其他线程修改,为避免线程冲突,释放锁并重新进入retry流程
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}

// 避免弱引用的死锁,并通过+initialize构造器保证所有的弱引用的isa指针都被初始化
if (haveNew && newObj) {
// 获取newObj的isa指针
Class cls = newObj->getIsa();
// 如果isa指针改变或者未初始化,进入if语句内的流程进行初始化
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// isa指针初始化
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));

// 如果该类已经执行完毕+initialize方法或者类正在执行+initialize方法,设置previouslyInitializedClass的值为cls,并进入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

// 如果newObj非空且不是TaggedPointer类型,在引用计数表中设置newObj的weak referenced bit位
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}

// 为避免多线程竞争,只在这里设置location指向newObj
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}

SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

return (id)newObj;
}

首先定义了一个模板参数,包含了三个参数:

  • HaveOld true: 变量已经有值,需要先清理;false: 变量没有值,无需清理
  • HaveNew true: 有一个新的值需要分配给变量,当前值可能为nil;false: 没有需要分配的新值
  • CrashIfDeallocating true: 当newObj正在释放或者newObj不支持弱引用时,进程停止;false: 进程不停止,存储nil代替。

然后从SideTables中取出相应的oldTable和newTable。

1
2
3
4
5
6
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

alignas(StripedMap<SideTable>) static uint8_t
SideTableBuf[sizeof(StripedMap<SideTable>)];

在SideTables()函数中使用了C++的强制类型转换符:reinterpret_cast

1
2
// 这里的type_id 必须是一个指针、引用、算术类型、函数指针或者成员指针
reinterpret_cast <type_id> (expression)

它把expression转换成type_id类型,但是不做任何类型检查和转换,其结果只是简单地从一个指针到另一个指针的值的二进制copy,通常用于底层代码。

StripedMap是一个模板类(Template Class),通过传入类(结构体)参数,会动态修改在该类中的一个array成员存储的元素类型,并且其中提供了一个针对于地址的hash算法,用作存储key。可以说,StripedMap提供了一套拥有将地址作为key的hash table解决方案,而且该方案采用了模板类,是拥有泛型性的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif

struct PaddedT {
T value alignas(CacheLineSize);
};

PaddedT array[StripeCount];

// hash算法,从ptr地址计算对应的key,这里的key为数组下标
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

public:
// array取值
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}

// Shortcuts for StripedMaps of locks.
...

constexpr StripedMap() {}
};

看下SideTable的内部结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;

SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}

~SideTable() {
_objc_fatal("Do not delete SideTable.");
}

// lock相关的代码,省略
...
};

SideTable结构体中定义了如下内容:

  • spinlock_t slock 自旋锁,用来保证多线程下的原子操作
  • RefcountMap refcnts 引用计数表,Hash表
  • weak_table_t weak_table 弱引用表,Hash表
  • 构造函数 其中包含了weak_table的初始化代码
  • 析构函数
  • 加锁、解锁等:lock()/unlock()
  • 对oldTable和newTable进行加锁、解锁操作的函数:lockTwo/unlockTwo

RefcountMap是runtime中用于存储引用计数的hash表,它使用DenseMap数据结构来实现:

1
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

DenseMap的讨论超出了本篇的范围,这里我们只需要知道DenseMap是在llvm中定义并广泛使用的一种数据结构,它本身的实现是一个基于Quadratic probing(二次探查)的散列表,键值对本身是std::pair<KeyT, ValueT>。想看相关源码的同学可以戳这里:llvm-Densemap.h

2.2 weak_table_t

全局的弱引用散列表,存储了所有weak变量(weak变量实际存储在weak_entry_t中,与对象相关联,下文详细介绍),先看下它的组成:

1
2
3
4
5
6
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
  • weak_entry_t *weak_entries 指向了所有对象的所有weak变量存储区域的入口
  • size_t num_entries 存储空间大小
  • uintptr_t mask
  • uintptr_t max_hash_displacement 对应key的值在hash表中的最大偏移值

weak_table_t使用hash表存储所有的weak变量,hash key是需要进行weak引用的对象,value是对象对应的entry(weak_entry_t结构体),entry中使用数组或hash表的方式存储了与对象相关的所有weak变量的指针。

看下weak_entry_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2; // 64位系统下占用62bit,记录referent的弱引用变量的数量
uintptr_t mask; // 记录当前referrers容器的大小
uintptr_t max_hash_displacement; // 根据hash-key寻找index的最大移动次数
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};

bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}

weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}

weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};

逐个成员变量看:

  • DisguisedPtr<objc_object> referent 封装为DisguisedPtr类型的对象指针referent,该指针即指向需要进行弱引用的对象,封装的目的是为了解决weak hash表可能导致的内存泄露的问题。

  • union联合体 这里首先要明白union的一些概念,union的成员变量都是在同一地址存放的,使成员变量相互覆盖,共同占用同一段内存。union具有以下特点:

    1、union同一个内存段可以用来存放多种不同类型的成员,但是同一时刻只有一个成员起作用
    2、union中起作用的是最后一个存放的成员,在存入一个新成员后,原有的变量就会失去作用
    3、union中所有成员的起始地址相同
    4、union占用的内存长度等于最长的成员的内存长度,而struct的内存长度为其中所有成员变量占用的内存之和

    这里的union中定义了两个struct成员变量,这两个struct用于不同情形下的weak弱引用指针的存储。

  1. out_of_line_ness != REFERRERS_OUT_OF_LINE时,weak_entry_t中存储二维指针的区域是一个名称为inline_referrers的数组,该数组中的元素为weak指针的指针。

  2. out_of_line_ness == REFERRERS_OUT_OF_LINE时,weak_entry_t中存储weak二维指针的区域为一块特定的内存区域,referrers指向这块内存区域的起始地址,在这块内存的存取函数中定义了相关的hash key函数以及冲突解决策略等,将这块内存区域构造成一个hash set使用(后文再详细展开)。

// out_of_line_ness field overlaps with the low two bits of inline_referrers[1].
// inline_referrers[1] is a DisguisedPtr of a pointer-aligned address.
// The low two bits of a pointer-aligned DisguisedPtr will always be 0b00
// (disguised nil or 0x80..00) or 0b11 (any other address).
// Therefore out_of_line_ness == 0b10 is used to mark the out-of-line state.

out_of_line_ness变量与inline_referrers[1]的最低两位重叠(inline_referrers[1]也是一个封装为DisguisedPtr类型的指针),在源码的注释中也说了,out_of_line_nessinline_referrers[1]的最低2位重合,在64位系统下,out_of_line_nessnum_refs一共占用64bit,由于此时内存结构刚好对齐(out_of_line_nessnum_refs的长度加起来刚好与inline_referrers[1]的长度相同),所以下一个元素mask的内存地址刚好换行。

weak_referrer_t是一个封装为DisguisedPtrobjc_object类型的二维指针(objc_object **)

1
typedef objc_object ** weak_referrer_t

看下weak_entry_t的构造函数:

1
2
3
4
5
6
7
8
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}

在创建weak_entry_t实例时,默认使用inline_referrers数组的方式来存储weak指针的指针,并把其余位上的数据清空。至此可以做出如下推断:当对象的weak变量的个数小于WEAK_INLINE_COUNT时,weak_entry_t中使用简单的inline数组(即:inline_referrers)方式来存储该对象的这些weak变量的指针,当inline_referrers数组装满之后,out_of_line_ness被设置为REFERRERS_OUT_OF_LINE,这时如果该对象有更多的weak变量,使用out_of_inline的方式存储(存储于hash表)。这么做也是基于性能的考虑,很明显当对象有大量的weak变量时,hash表的存取效率是优于数组的。

2.5 小结

通过上文的分析,可以看出对象的weak变量的存储结构,首先是两个结构体:weak_tale_tweak_entry_t,它们中分别定义了一个hash表,其中weak_tale_t存储多个weak_entry_t,以hash表的形式存取,weak_entry_t存储某个对象的所有弱引用变量的指针(二维指针的形式),它里面也有一个hash表(当弱引用变量的数量<=4时,以数组的方式存取)。

weak变量的存储结构

三、部分细节分析

既然上文说了weak变量都是以hash表的形式存取,自然而然地能想到这些问题:

  • hash表是怎么实现的
  • hash函数是怎样的
  • 怎样解决hash冲突
  • hash表是如何存取、删除的

带着这些问题,开始下面的分析

3.1 hash key计算

看下objc-weak.mm文件中有两个hash函数:

1
2
3
4
5
6
7
static inline uintptr_t hash_pointer(objc_object *key) {
return ptr_hash((uintptr_t)key);
}

static inline uintptr_t w_hash_pointer(objc_object **key) {
return ptr_hash((uintptr_t)key);
}

这两个hash函数根据对象的指针或对象指针的指针,通过ptr_hash函数生成key,这个key就是存取hash表的关键。

  • hash_pointer函数对应weak_table_t中的hash表(即weak_entries),根据被弱引用对象的地址生成key
  • w_hash_pointer函数对应weak_entry_t中的out_of_line时的referrers hash表,根据对象的weak变量的指针生成key

看下哈希函数ptr_hash

1
2
3
4
5
6
7
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}

ptr_hash基于一种Fash Hash算法,该算法一定程度地牺牲hash准确度以保证hash的速度,这个哈希函数相当的简洁,但却行之有效。至于为什么key >> 4,猜测是malloc分配内存后的指针都有16字节偏移的原因,在这种情形下key >> 4能够达到很低的碰撞率,具体的讨论可以看这里:[llvm-dev] Default hashing function for integers (DenseMapInfo.h)。有关malloc指针的16字节偏移的问题没有深入思考过,等以后有时间再进行吧。

3.2 weak_entry_t插入

grow_refs_and_insert函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
__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为8,然后以2倍大小进行增长
size_t new_size = old_size ? old_size * 2 : 8;

size_t num_refs = entry->num_refs;
weak_referrer_t *old_refs = entry->referrers;
// 新的容器大小为new_size,增加一个元素后,size-1并赋值给entry->mask
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;
// 将老数据逐个复制到新的entry存储空间中
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
// entry插入新的数据
append_referrer(entry, new_referrer);
// 释放老的存储空间
if (old_refs) free(old_refs);
}

weak_entry_t中hash表新增数据时,首先创建一个新的存储空间(大小为old_size * 2),然后将所有的老数据复制到新的空间中,然后再将新的数据插入到新的空间中,最后释放老的存储空间。为什么要这么做呢?我们知道C数组的长度在创建的时候就固定了的,为了能够动态地向数组中添加新元素,就需要不断地申请新的内存空间(并且要求是连续的内存地址),这样就必须把老的数据复制到新申请的内存空间中,然后再加入新增的数据,最后释放掉原内存空间。同时,为了避免频繁地申请新的内存空间和复制数据,将新内存空间大小的增长设置为原来的2倍。

grow_refs_and_insert函数使用append_referrer对entry进行数据插入操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
if (! entry->out_of_line()) {
// Try to insert inline.
// 先使用inline的方式增加新的弱引用,将新数据增加到inline_referrers数组中
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.
// inline存储方式失败,使用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_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) {
return grow_refs_and_insert(entry, new_referrer);
}
// 关键代码
// 对new_referrer调用hash函数,获取key,将其赋值给begin
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 这里用了线性探测法解决hash冲突
while (entry->referrers[index] != nil) {
hash_displacement++;
index = (index+1) & entry->mask;
// 探测到begin的位置,出现这种问题的原因可能是:
// 1. runtime bug
// 2. 内存错误导致存储空间扩容失败
if (index == begin) bad_weak_table(entry);
}
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
// 当前index存储了新值可以插入的位置
weak_referrer_t &ref = entry->referrers[index];
// 插入新值并将weak引用数量+1
ref = new_referrer;
entry->num_refs++;
}

append_referrer函数的流程比较简单:

  1. 插入新值时先判断当前entry是否out_of_line,如果没有,使用inline的方式将新值存储到inline_referrers数组中,如果inline存储方式失败,转为out_of_line的方式,分配一块大小为WEAK_INLINE_COUNT的内存空间,将起始地址赋值给new_referrers
  2. 然后判断当前entry中的弱引用数量,如果超过了当前存储空间的3/4,对存储空间进行扩容。扩容的目的是避免hash表的装填因子过大,由于这里采用线性探测再散列法解决hash冲突,当装填因子非常接近于1时,线性探测类似于顺序查找,其性能相当于数组遍历,降低了hash表的存取性能;
  3. 最后是将新元素插入hash表,使用线性探测再散列找到可以插入的位置,插入新值并将num_refs加1。

细心的同学可能注意到了,在计算key和线性探测的过程中有这样两句代码:

1
2
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
index = (index+1) & entry->mask;

为何key和index的值要跟entry->mask相与呢?我们知道,entry->mask存储了referrers数组的大小,进行“与”操作后,所有超过referrers数组边界的二进制位都被置为0,避免了可能出现的数组越界的问题。

既然有插入操作,那么就有对应的删除操作:remove_referrer函数,这个函数与append_referrer的流程基本相同,不同的是它是找到hash表中相应的位置并将其值置空。这里就不详细分析了。

3.3 weak_table_t插入

weak_table_t的插入涉及到了插入、hash表的扩容等操作,相关的函数有:

  • weak_entry_insert
  • weak_grow_maybe
  • weak_resize
3.3.1 weak_entry_insert函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
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;
}
}

可以看到weak_entry_insert函数的流程与上文的append_referrer函数相似,只是省略了inline方式存储的尝试,说明weak_table_t中默认使用hash表的方式存储所有的entries实例。

3.3.2 weak_grow_maybe函数
1
2
3
4
5
6
7
8
9
static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);

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

weak_grow_maybe函数负责weak_table_t hash表的扩容,与weak_entry_t的策略相同:在hash表中存储的值数量大于hash表容量的3/4时,调用weak_resize函数将hash表扩容到之前容量的2倍。看下weak_resize函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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_entry_t的策略相似,不同的是weak_resize只将老数据移动到新的内存空间中,插入新数据的操作又交给了weak_entry_insert函数。

3.4 weak_table_t删除元素

1
2
3
4
5
6
7
8
9
10
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--;

weak_compact_maybe(weak_table);
}

先判断entry的存储方式是否out_of_line,如果是,直接释放掉referrers内存空间,然后调用sizeof(*entry)获取当前entry占用的内存空间大小,将这块内存全部置0,然后将num_entries减1,最后调用weak_compact_maybe函数缩小hash表的容量至当前容量的 1/8。只有同时满足以下两个条件时才收缩表的容量:1、当前表容量>=1024;2、当前表中entry的数量<=表容量的1/16。weak_compact_maybe的源码这里就不贴出来了。

3.5 对象与weak变量的绑定、解绑等

通过前文的分析,weak_table_t存储了与某个对象关联的所有弱引用entry(weak_entry_t),这种结构类似于键值对,那么这种对象-entry的键值对是如何创建并添加到weak_table_t中的,又是如何删除的呢?

3.5.1 weak_register_no_lock

weak_register_no_lock函数负责注册一个新的对象-entry键值对,如果新的对象不存在则去创建一个新的entry并将其与该对象绑定。这里只贴出关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;

// 检查对象是否有效以及是否TaggedPointer对象
if (!referent || referent->isTaggedPointer()) return referent_id;

// ensure that the referenced object is viable
...

// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 将对象的弱引用指针添加到entry中
append_referrer(entry, referrer);
}
else {
// 创建新的`weak_entry_t`实例,并将其与需要弱引用的对象关联
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}

return referent_id;
}

weak_register_no_lock函数做的事情主要有:

  1. 判断对象是否可用、是否TaggedPointer对象,如果对象不可用或者是TaggedPointer对象,直接返回被引用的对象自身。
  2. 确保需要进行弱引用的对象是可用的(没有释放或没有正在释放等)。
  3. 如果当前被引用的对象已存在对应的entry,调用append_referrer将弱引用指针添加到entry中。如果不存在,调用weak_entry_t的构造函数创建一个新的entry并与当前对象绑定,然后调用weak_grow_maybe对当前weak_table_t的hash表进行扩容(如果需要)。最后调用weak_entry_insert函数将新的entry插入到weak_table_t的hash表中。
3.5.2 weak_unregister_no_lock

有绑定操作就有与之对应的解绑操作,该函数的流程就不具体说了,看注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;
// 根据弱引用的对象去weak_table中查找对应的entry
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 删除entry中对应的弱引用指针
remove_referrer(entry, referrer);
bool empty = true;
// 判断inline或out_of_line两种存储方式下entry是否为空
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;
}
}
}
// 如果entry为空,将其从weak_table中删除
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
}

四、 结语

行文比较乱,这里自上而下简单总结一下,主要是两个结构体:

  • weak_table_t使用hash表的形式存储多个weak_entry_t实例(entry),并且hash表会根据entry的数量动态调整hash表的容量
  • weak_entry_t使用数组和hash表的形式存储与弱引用对象相关的所有弱引用变量(二维指针)

回到开篇提出的三个问题:

  • weak为什么不会增加对象的引用计数

我们知道,引用计数的增加离不开retain操作,而在storeWeak函数中并没有调用任何retain操作,当然也就不会使对象的引用计数增加了。对比下objc_storeStrong(对象的强引用最终调用objc_storeStrong函数):

1
2
3
4
5
6
7
8
9
10
11
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
  • weak变量存储在哪里

看上面的总结。

  • 对象释放时其weak指针如何自动设置为nil

对象释放时,runtime会调用相应的dealloc函数,而dealloc函数中会调用相应的weak指针置空函数:weak_clear_no_lock将与该对象相关的所有weak指针全部设置为nil,详情看代码注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
// 对象指针
objc_object *referent = (objc_object *)referent_id;
// 获取与对象关联的entry
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
return;
}

// zero out references
weak_referrer_t *referrers;
size_t count;

// 判断entry中弱引用变量的存储方式:数组还是hash表
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) {
// referrer为weak指针的指针
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
// 将对象的weak指针置为nil
*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();
}
}
}
// 从weak_table的hash表中移除对象的相应entry
weak_entry_remove(weak_table, entry);
}