Wrappres' Studio.

iOS 中关于 weak 的故事

字数统计: 2.3k阅读时长: 9 min
2019/05/09 Share

weak

iOS 中 weak 是不得不了解的内容
希望能通过本文把 weak 这件事讲清楚

作用

在所有使用引用计数的语言中,都会面临一个问题,那就是循环引用。两个对象互相引用,或多个对象之间,形成环形引用,造成类似死锁互相等待的情况,导致对象一直无法被释放。引入 weak 旨在解决循环引用的问题。

原理

SideTables

SideTables 是一个全局的 hash 数组,里面存储着 SideTable 数据,且长度为 64。因此可以说,一个对象对应一个 SideTable ,但一个 SideTable 有时也会对应多个对象,因为 SideTables 的长度只有 64,因此很多对象会对应同一个 SideTable。

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

SideTable

SideTable 用来管理某个对象的引用计数表 refcnts 和 weak 表 weak_table。

1
2
3
4
5
6
7
8
9
struct SideTable {
// 自旋锁,用来保证线程安全
spinlock_t slock;
// 引用计数表
RefcountMap refcnts;
// weak 表
weak_table_t weak_table;
...
};

weak_table_t

weak_table 中存储了对象的弱引用信息。我们知道 SideTables 长度只有 64,但一般情况下,都不会只有 64 个对象,因此常常会出现多个对象公用同一个 SideTable 的情况。也就是说一个 weak_table 会存储多个对象的弱引用信息。因此在同一个 SideTable 中,又会通过 weak_table 作为 hash 表再次分散各个对象的弱应用信息。

1
2
3
4
5
6
7
8
9
10
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间,即 entries 的数目
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移量
uintptr_t max_hash_displacement;
};

其中 weak_entries 是一个动态数组,其余三个元素用于 hash 表的相关操作。

weak_entry_t

每个 weak_entry_t 存储着一个对象的弱引用信息。weak_entry_t 的结构与 weak_table_t 很像,同样也是一个 hash 表,其中存储的元素是 weak_referrer_t ,实质是弱引用对象指针的指针。通过操作指针的指针,可以实现 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
47
48
49
50
51
52
/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line_ness != REFERRERS_OUT_OF_LINE then the set
* is instead a small inline array.
*/
#define WEAK_INLINE_COUNT 4

// 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.
#define REFERRERS_OUT_OF_LINE 2

struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被弱引用的对象

// 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers
union {
struct {
weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};
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:弱引用对象指针摘要。其实可以理解为弱引用对象的指针,只不过这里使用了摘要的形式存储。(所谓摘要,其实是把地址取负)。
  • union:接下来是一个联合,union 有两种形式:定长数组 weak_referrer_t inline_referrers[WEAK_INLINE_COUNT] 和 动态数组 weak_referrer_t *referrers。这两个数组是用来存储弱引用该对象的指针的指针的,同样也使用了指针摘要的形式存储。当弱引用该对象的指针数目小于等于 WEAK_INLINE_COUNT 时,使用定长数组。当超过 WEAK_INLINE_COUNT 时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。
  • *bool out_of_line():该方法用来判断当前的 weak_entry_t 是使用的定长数组还是动态数组。当返回 true,此时使用的动态数组,当返回 false,使用静态数组。
  • weak_entry_t& operator=(const weak_entry_t& other):赋值方法。
  • weak_entry_t(objc_object *newReferent, objc_object**newReferrer):构造方法。

流程

初始化

初始化时,runtime 会调用 objc_initWeak 初始化一个新的 weak 指针地址指向对象地址。

1
2
3
4
5
6
7
8
9
10
11
12
id objc_initWeak(id *location, id newObj) {
// 查看对象实例是否有效
// 无效对象直接导致指针释放
if (!newObj) {
*location = nil;
return nil;
}
// 这里传递了三个 bool 数值
// 使用 template 进行常量参数传递是为了优化性能
return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}

添加引用

objc_initWeak 函数会调用 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
// HaveOld:     true - 变量有值
// false - 需要被及时清理,当前值可能为 nil
// HaveNew: true - 需要被分配的新值,当前值可能为 nil
// false - 不需要分配新值
// CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停
// false - 用 nil 替代存储
template bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj) {
// 该过程用来更新弱引用指针的指向
// 初始化 previouslyInitializedClass 指针
Class previouslyInitializedClass = nil;
id oldObj;
// 声明两个 SideTable
// ① 新旧散列创建
SideTable *oldTable;
SideTable *newTable;
// 获得新值和旧值的锁存位置(用地址作为唯一标示)
// 通过地址来建立索引标志,防止桶重复
// 下面指向的操作会改变旧值
retry:
if (HaveOld) {
// 更改指针,获得以 oldObj 为索引所存储的值地址
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (HaveNew) {
// 更改新值指针,获得以 newObj 为索引所存储的值地址
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 加锁操作,防止多线程中竞争冲突
SideTable::lockTwoHaveOld, HaveNew>(oldTable, newTable);
// 避免线程冲突重处理
// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
if (HaveOld && *location != oldObj) {
SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
goto retry;
}
// 防止弱引用间死锁
// 并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向
if (HaveNew && newObj) {
// 获得新对象的 isa 指针
Class cls = newObj->getIsa();
// 判断 isa 非空且已经初始化
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized()) {
// 解锁
SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
// 对其 isa 指针进行初始化
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// 如果该类已经完成执行 +initialize 方法是最理想情况
// 如果该类 +initialize 在线程中
// 例如 +initialize 正在调用 storeWeak 方法
// 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记
previouslyInitializedClass = cls;
// 重新尝试
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);
// 如果弱引用被释放 weak_register_no_lock 方法返回 nil
// 在引用计数表中设置若引用标记位
if (newObj && !newObj->isTaggedPointer()) {
// 弱引用位初始化操作
// 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
newObj->setWeaklyReferenced_nolock();
}
// 之前不要设置 location 对象,这里需要更改指针指向
*location = (id)newObj;
}
else {
// 没有新值,则无需更改
}
SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
return (id)newObj;
}

释放

  1. 调用 objc_release
  2. 因为对象的引用计数为 0,所以执行 dealloc
  3. 在 dealloc 中,调用了_objc_rootDealloc 函数
  4. 在_objc_rootDealloc 中,调用了 object_dispose 函数
  5. 调用 objc_destructInstance
  6. 最后调用 objc_clear_deallocating

其中 objc_clear_deallocating :

1
2
3
4
5
6
7
void  objc_clear_deallocating(id obj) 
{
assert(obj);
assert(!UseGC);
if (obj->isTaggedPointer()) return;
obj->clearDeallocating();
}

其中 clearDeallocating :

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
/**
* Called by dealloc; nils out all weak pointers that point to the
* provided object so that they can no longer be used.
*
* @param weak_table
* @param referent The object being deallocated.
*/
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
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) {
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();
}
}
}
weak_entry_remove(weak_table, entry);
}

  1. 从 weak 表中获取废弃对象的地址为键值的记录。
  2. 将包含在记录中的所有附有__weak 修饰符变量的地址,赋值为 nil
  3. 从 weak 表中删除记录
  4. 从引用计数表中删除废弃对象的地址作为键值的记录
CATALOG
  1. 1. weak
    1. 1.1. 作用
    2. 1.2. 原理
      1. 1.2.1. SideTables
      2. 1.2.2. SideTable
      3. 1.2.3. weak_table_t
      4. 1.2.4. weak_entry_t
    3. 1.3. 流程
      1. 1.3.1. 初始化
      2. 1.3.2. 添加引用
      3. 1.3.3. 释放