李峰峰博客

引用计数实现原理

2020-08-15

一、iOS 引用计数概述

对象的引用方式分为强引用弱引用,对象的强引用和弱引用信息保存在 SideTables 中,SideTables 是全局的哈希数组,里面存储了有限数量的 SideTable,多个对象会共用一个 SideTable,也就是说每个 SideTable 中存储了多个对象的引用计数信息。

底层通过 indexForPointer 函数计算某个对象使用的是 SideTables 数组中的哪个 index 位置的 SideTable

1
2
3
4
5
6
7
8
9
10
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

计算结果即为对象对应的 SideTableSideTables 中的 index

根据以上逻辑,可以看到是对对象地址 addr 经过一定计算后,将结果与 StripeCount 进行模运算。所以,最终计算得到的 index 小于 StripeCount,即 SideTables 中共存储 StripeCountSideTable,根据 StripeCount 定义可知,iPhone 和模拟器上,SideTables 存储 8 个 SideTable,Mac 上存储 64 个 SideTable

既然 SideTable 中存储了多个对象的引用计数信息,Apple 为什么要使用多个 SideTable,而不是直接在一个 SideTable 中存储全部对象的引用计数信息?

因为 SideTable 里有一个锁,如果把所有的类的引用信息都放在同一个 SideTable,有任何一个类有改动都会对整个 SideTable 做操作,并且在操作一个类的同时,操作别的类会被锁住等待,这样会导致操作效率和查询效率都很低。而有多个 SideTable 的话,操作的都是单个 SideTable,并不会影响其他的 SideTable,这样就避免了资源竞争,提高了效率。这里引入了分离锁的概念,简而言之就是将一张表分拆成多张表,对他们分别加锁,可以实现并发操作,提升执行效率。

SideTable 的数据结构:

1
2
3
4
5
6
7
8
struct SideTable {
// 锁
spinlock_t slock;
// 强引用 hash 表
RefcountMap refcnts;
// weak 引用 hash 表
weak_table_t weak_table;
};

需要注意的是,源码中对锁的定义使用的是 spinlock_t 类型的变量,看起来是自旋锁,实际上,Apple 已经弃用 OSSpinLock 了,最新源码是用 os_unfair_lock 来实现的,其底层执行 lockunlock 的其实是 mutex_t,也就是互斥锁:

1
2
3
4
5
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
// ......
}

二、强引用计数的存储

我们都知道,在非 ARC 情况下,需要开发者手动调用 retainrelease 进行内存管理,并且可以调用 retainCount 去获取对象的引用计数。

开始之前先看下 isa 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19
};
};

其中,引用计数器是存储在 extra_rc 中的(存储的是引用计数减 1),但是可以看到,extra_rc 只有 19 位,可以存储的最大引用计数:2^19-1+1=524288,可能会出现不够存储的情况,即存储计数溢出。

如果出现 extra_rc 不够存储引用计数的情况,has_sidetable_rc 将会被赋值为 1,并且引用计数会存在 SideTablerefcnts 中。

refcnts 是一个存放着对象引用计数的散列表,keyobjc_object,即 OC 对象,value 为引用计数,当 value 为 0 的时候,会将该记录从表中移除。所以,在 64bit 中,引用计数可以直接存储在优化过的 isa 指针中,也可能存储在 SideTable 中。

接下来看下 retainCount 这方法的实现:

1
2
3
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}

可以看到,其内部是调用 rootRetainCount() 方法。

在 ARC 环境下,可以调用 Core Foundation 的 CFGetRetainCount() 方法,或调用 Runtime 的 _objc_rootRetainCount(id obj) 方法来获取引用计数,它们实内部实际上也是调用 rootRetainCount() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
inline uintptr_t 
objc_object::rootRetainCount()
{
// Tagged Pointer,直接返回 isa 本身
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);

if (bits.nonpointer) {
// 开启了 isa 指针优化
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}

// 没有开启 isa 指针优化
sidetable_unlock();
return sidetable_retainCount();
}

可以看到 rootRetainCount 主要逻辑:

  • 如果是 Tagged Pointer,直接返回 isa 本身
  • 如果开启了 isa 指针优化
    • 先去 isa 中取出 extra_rc 存储的引用计数,并 + 1
    • 再去判断 isahas_sidetable_rc 是否为 1,如果为 1,就去对应 SideTable 中取出引用计数,再与上一步取到的引用计数相加返回
  • 如果没有开启 isa 指针优化,直接去对应 SideTable 中取出引用计数返回

上面 sidetable_getExtraRC_nolocksidetable_retainCount 都是去 SideTable 中取引用计数,两个函数主要逻辑基本类似,下面是 sidetable_getExtraRC_nolock 函数实现:

1
2
3
4
5
6
7
8
9
size_t 
objc_object::sidetable_getExtraRC_nolock()
{
assert(isa.nonpointer);
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}

可以看到,强引用计数是存储在对应 SideTable 中的 refcnts 中的。

根据 retainCount 的实现也能大致反推出 retainrelease 是如何修改引用计数的,这里不再补充。

三、weak 的实现原理

1、weak 引用计数的存储

(1) weak_table_t

前面提到多个对象会共用一个 SideTable,所以又会以 weak_table 作为 hash 表分散各个对象的弱应用信息,weak_table 是一个结构体:

1
2
3
4
5
6
7
8
9
10
struct weak_table_t { 
// 保存了全部 weak_entry_t 的 hash 数组
weak_entry_t *weak_entries;
// hash 数组中保存的 weak_entry_t 元素的个数
size_t num_entries;
// hash 数组的长度
uintptr_t mask;
// hash 冲突的最大次数
uintptr_t max_hash_displacement;
};

其中 weak_entries 是一个动态数组,其余三个元素用于 hash 表的相关操作,其中 mask = hash 数组长度 - 1,通过对象获取对应 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
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 begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}

return &weak_table->weak_entries[index];
}

以上源码主要是计算对象对应的 weak_entry_tweak_entries 数组中的 index,并根据 index 取出 weak_entry_thash_pointer 就是对对象 referent 的地址做个 hash,然后和 mask 做与运算,返回的结果小于 weak_table->mask ,保证了 index 不会越界,当计算结果做数组的索引。

这里 hash 冲突的解决方案是计算出 hash 位置 index,判断 index 对应的 weak_entry_t 中存储的 referent(即弱引用该“对象的指针”的指针) 是否与目标 referent 相等,不相等的话后移一位继续判断,并将 hash_displacement ++,记录移动次数。当 hash_displacement 的值大于 max_hash_displacement 时,直接返回 nil。当 index == begin 时,即遍历一圈也没找到目标对象,直接调用 bad_weak_table 报错。

(2) weak_entry_t

每个 weak_entry_t 存储着一个对象的弱引用信息。weak_entry_t 的结构与 weak_table_t 类似,也是一个 hash 表。其中存储的元素是 weak_referrer_t ,实质是弱引用对象指针的指针。通过操作指针的指针,可以实现 weak 引用的指针在对象析构后,指向 nil

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
35
36
37
38
39
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被弱引用的对象

// 引用该对象的对象列表,联合体
weak_referrer_t *referrers
union {
struct {
weak_referrer_t *referrers; // 动态数组,引用计数大于 4 时使用
uintptr_t out_of_line_ness : 2; // 是否使用动态 hash 数组标记位
uintptr_t num_refs : PTR_MINUS_2; // hash 数组中的元素个数
uintptr_t mask; // hash 数组长度 -1(hash 数组的长度,而不是元素实际个数)
uintptr_t max_hash_displacement; // 可能会发生的 hash 冲突的最大次数
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; // 定长数组,引用计数小于 4 时使用
};
};

// 判断当前的 weak_entry_t 是使用的定长数组还是动态数组
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;
}
}
};

其中 WEAK_INLINE_COUNT 定义如下:

1
#define WEAK_INLINE_COUNT 4

其中 DisguisedPtr<objc_object> referent 为弱引用对象指针摘要,可以简单理解为被弱引用的对象,只不过这里使用了摘要的形式存储(所谓摘要,其实是把地址取负)。

2、weak 实现流程

(1) 初始化

当我们初始化一个 weak 对象时,runtime 会调用 objc_initWeak 函数:

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

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

该方法有两个参数 locationnewObj

  • location
    __weak 指针的地址
  • newObj
    所引用的对象

(2) 添加引用

objc_initWeak 函数会调用 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;
}

storeWeak 函数用来更新弱引用指针的指向,创建对应的弱引用表:

  • storeWeak 方法实际上是接收的参数,分别是 haveOldhaveNewcrashIfDeallocating ,这三个参数都是以模板的方式传入的,是三个 bool 类型的参数。 分别表示 weak 指针之前是否指向了一个弱引用、weak 指针是否需要指向一个新的引用、若果被弱引用的对象正在析构,此时再弱引用该对象是否应该 crash。
  • 该方法维护了 oldTablenewTable 分别表示旧的引用弱表和新的弱引用表,它们都是 SideTable 的 hash 表。
  • 如果 weak 指针之前指向了一个弱引用,则会调用 weak_unregister_no_lock 函数将旧的 weak 指针地址移除。
  • 如果 weak 指针需要指向一个新的引用,则会调用 weak_register_no_lock 函数将新的 weak 指针地址添加到弱引用表中。
  • 调用 setWeaklyReferenced_nolock 方法修改 weak 新引用的对象的 bit 标志位。

这里涉及到两个关键的函数:

  • weak_unregister_no_lock
    weak ptr 地址 从 objweak_entry_t 中移除
  • weak_register_no_lock
    weak ptr 地址 注册到 obj 对应的 weak_entry_t

其中 weak_register_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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;

// 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作
if (!referent || referent->isTaggedPointer()) return referent_id;

// 确保被引用的对象可用(没有在析构,同时应该支持weak引用)
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_table中找到referent对应的weak_entry,并将referrer加入到weak_entry中
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) { // 如果能找到weak_entry,则讲referrer插入到weak_entry中
append_referrer(entry, referrer); // 将referrer插入到weak_entry_t的引用数组中
}
else { // 如果找不到,就新建一个
weak_entry_t new_entry(referent, referrer);
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;
}
  • 若引用计数使用了 taggedPointer,则不会做任何引用计数。
  • 判断 referent_id 是否正在被析构以及 referent_id 是否支持 weak 引用。如果 referent_id 不能够被 weak 引用,则直接返回 nil
  • 如果 referent_id 能够被 weak 引用,则将 referent_id 对应的 weak_entry_tweak_tableweak_entry_t 哈希数组中找出来,如果不存在,则新建。
  • 调用 append_referrer 函数将 referrer 插入到 weak_entry_t 的引用数组中。
  • weak_entry_t 插入到 weak_table 中,并返回 referent_id

referrer 插入到 weak_entry_t 中调用了函数 append_referrer

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
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
if (! entry->out_of_line()) { // 如果 weak_entry 尚未使用动态数组,走这里
// 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;
}
}

// 如果 inline_referrers 的位置已经存满了,则要转型为 referrers,做动态数组。
// 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_ness = REFERRERS_OUT_OF_LINE;
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) { // 如果动态数组中元素个数大于或等于数组位置总空间的3/4,则扩展数组空间为当前长度的一倍
return grow_refs_and_insert(entry, new_referrer); // 扩容,并插入
}

// 如果不需要扩容,直接插入到 weak_entry 中
// 注意,weak_entry 是一个哈希表,key:w_hash_pointer(new_referrer) value: new_referrer

// 这里 weak_entry_t 的 hash 算法和 weak_table_t 的 hash 算法是一样的,同时扩容/减容的算法也是一样的
size_t begin = w_hash_pointer(new_referrer) & (entry->mask); // '& (entry->mask)' 确保了 begin的位置只能大于或等于 数组的长度
size_t index = begin; // 初始的 hash index
size_t hash_displacement = 0; // 用于记录 hash 冲突的次数,也就是 hash 再位移的次数
while (entry->referrers[index] != nil) {
hash_displacement++;
index = (index+1) & entry->mask; // index + 1, 移到下一个位置,再试一次能否插入
if (index == begin) bad_weak_table(entry); // index == begin 意味着数组绕了一圈都没有找到合适位置,报错处理。
}
if (hash_displacement > entry->max_hash_displacement) { // 记录最大的hash冲突次数, max_hash_displacement意味着: 我们尝试至多max_hash_displacement次,肯定能够找到object对应的hash位置
entry->max_hash_displacement = hash_displacement;
}
// 将 ref 存入 hash 数组,同时,更新元素个数 num_refs
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
entry->num_refs++;
}
  • 如果 weak_entry 未使用动态数组,且静态数组还有空间,直接找空位存放。
  • 如果没有位置存放,则升级为动态数组,如果动态数组中元素个数大于或等于数组位置总空间的 3/4,则数组空间扩容当前空间大小 1 倍。
  • 插入到 weak_entry 中。

如果 weak 指针之前指向了一个弱引用,则会调用 weak_unregister_no_lock 方法将旧的 weak 指针地址移除,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
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))) { // 查找到referent所对应的weak_entry_t
remove_referrer(entry, referrer); // 在referent所对应的weak_entry_t的hash数组中,移除referrer

// 移除元素之后, 要检查一下weak_entry_t的hash数组是否已经空了
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_t的hash数组已经空了,则需要将weak_entry_t从weak_table中移除
weak_entry_remove(weak_table, entry);
}
}
  • weak_table 中找出 referent 对应的 weak_entry_t
  • weak_entry_t 中移除 referrer
  • 判断 weak_entry_t 中是否还有元素,如果已经没有元素了,则将 weak_entry_tweak_table 中移除。

(1)(2)步总结如下:

  • 1、调用 indexForPointer 获取对象所用的 SideTableSideTables 中的 index,从而取到 SideTable

    • SideTables 是全局 hash 数组,在 iPhone 和模拟器上,SideTables 存储 8 个 SideTable,Mac 上存储 64 个 SideTableSideTable 数量即 StripeCount
    • 计算 index 时,将对象地址经过一定位移计算后,与 StripeCount 进行模运算,结果即 index,同时确保了结果不会超过 StripeCount
  • 2、调用 weak_entry_for_referentweak_table 中的 weak_entries 中获取对应 weak_entry_t

    • weak_table 中的 weak_entries 同样是个 hash 数组,里面存储了多个 weak_entry_t,每个 weak_entry_t 存储 1 个对象的弱引用信息。
    • 计算 weak_entry_tweak_entries 数组中的 index 方式:将对象地址经过 hash_pointer 函数进行 hash,然后和 mask 进行与运算后得到 index,保证了 index 不会越界。
      如果对应 index 位置已经有 weak_entry_t 了,就取出其中的 referent(即弱引用对象指针的指针) 是否与目标 referent 相等,不相等的话后移一位继续判断,并将 hash_displacement ++,记录移动次数。当 hash_displacement 的值大于 max_hash_displacement 时,直接返回 nil
      index == begin 时,即遍历一圈也没找到目标对象,直接调用 bad_weak_table 报错。
  • 3、runtime 调用 objc_initWeak 初始化 weak 指针地址指向对象地址,objc_initWeak 中调用 storeWeak,相关主要逻辑在 storeWeak 中。

    • 如果 weak 指针之前指向了一个弱引用,则会调用 weak_unregister_no_lock 函数将旧的 weak 指针地址移除。
    • 如果 weak 指针需要指向一个新的引用,则会调用 weak_register_no_lock 函数将新的 weak 指针地址添加到弱引用表中。
    • weak_register_no_lock 函数中调用 append_referrer 函数将 referrer(弱引用对象指针的指针)插入到 weak_entry_t 的引用数组。
    • append_referrer 中会判断 weak_entry 是否是使用了动态数组及动态数组是否需要扩容(当前已使用空间大于等于空间 3/4 将进行扩容,扩容使空间翻倍)

四、对象的释放

当对象引用计数变为 0 时,其 dealloc 方法会被调用,下面是 LLVM 官方文档 对 dealloc 过程的一个描述:

A class may provide a method definition for an instance method named dealloc. This method will be called after the final release of the object but before it is deallocated or any of its instance variables are destroyed. The superclass’s implementation of dealloc will be called automatically when the method returns.

大致意思是:dealloc 方法在对象释放(最后一次 release)之后调用,但是对象的实例变量(Ivars)此时还没有释放。dealloc 方法返回时,会自动调用父类的 dealloc 方法。

上面提到,对象释放后,会调用 dealloc 方法,其内部的实例变量还未销毁,那它的实例变量何时销毁呢?文档中有相关介绍:

The instance variables for an ARC-compiled class will be destroyed at some point after control enters the dealloc method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.

也就是说,ARC 环境下,实例变量在调用根类的 dealloc 方法([NSObject dealloc])后的某个时刻被销毁。但实例变量的销毁顺序是不固定的,无论是在单个类内还是在子类和父类之间。

下面是 dealloc 方法的实现:

1
2
3
- (void)dealloc {
_objc_rootDealloc(self);
}

dealloc 方法内部调用了 _objc_rootDealloc 函数:

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
void
_objc_rootDealloc(id obj)
{
assert(obj);

obj->rootDealloc();
}

_objc_rootDealloc 又会调用 rootDealloc 方法:
inline void
objc_object::rootDealloc()
{
// 如果是 Tagged Pointer,直接返回
if (isTaggedPointer()) return; // fixme necessary?

// 没有开启指针优化 & 对象没有被 weak 引用 & 没有关联对象 & 没有自定义的 C++ 析构方法 & 没有用到 sideTable 来做引用计数
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
// 快速释放
assert(!sidetable_present());
free(this);
}
else {
// 慢速释放
object_dispose((id)this);
}
}

根据以上逻辑可以知道,接下来会调用 object_dispose 函数:

1
2
3
4
5
6
7
8
9
10
11
id 
object_dispose(id obj)
{
if (!obj) return nil;
// 析构obj
objc_destructInstance(obj);
// 释放内存
free(obj);

return nil;
}

其内部主要调用了 objc_destructInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
// c++ 析构函数
bool cxx = obj->hasCxxDtor();
// 判断是否有关联对象
bool assoc = obj->hasAssociatedObjects();

// 如果有 c++ 析构函数 则调用 c++ 析构函数.
if (cxx)
object_cxxDestruct(obj);

// 如果有关联对象则移除关联对象
if (assoc)
_object_remove_assocations(obj);
// 清理相关的引用
obj->clearDeallocating();
}

return obj;
}

清理相关引用逻辑主要是在 clearDeallocating 中实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inline void 
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// 没有开启 isa 指针优化 清理 obj 存储在 sideTable 中的引用计数等信息
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// 开启了 isa 指针优化,或者使用了 sideTable
// 清理强引用和弱引用信息
clearDeallocating_slow();
}

assert(!sidetable_present());
}

接下来看下 clearDeallocating_slow 函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

// 找到对应的 SideTable
SideTable& table = SideTables()[this];
table.lock();

// 判断是否需要清理弱引用信息
if (isa.weakly_referenced) {
// 对象被弱引用
// 在 SideTable 的 weak_table 中对 this 进行清理工作
weak_clear_no_lock(&table.weak_table, (id)this);
}

// 判断是否需要清理强引用信息
if (isa.has_sidetable_rc) {
// 采用了 SideTable 做引用计数
// 在 SideTable 的引用计数中移除 this
table.refcnts.erase(this);
}
table.unlock();
}

上面逻辑,如果对象被弱引用,调用 weak_clear_no_lock 清理 weak_table。如果对象采用了 SideTable 强引用计数,则移除相关强引用信息。

接下来看下 weak_clear_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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 清理 weak_table,并将所有 weak 指针置为 nil
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;

// 找到 referent 在 weak_table 中对应的 weak_entry_t
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;
}

// 找出 weak 引用 referent 的 weak 指针地址数组以及数组长度
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;
}

// 遍历所有的所引用 weak 指针
for (size_t i = 0; i < count; ++i) {
// 取出每个 weak ptr 的地址
objc_object **referrer = referrers[i];

if (referrer) {
// 如果 weak ptr 确实 weak 引用了referent,则将 weak ptr 设置为 nil,这也就是为什么 weak 指针会自动设置为nil的原因
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
// 如果所存储的weak ptr没有weak 引用referent,这可能是由于runtime代码的逻辑错误引起的,报错
_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();
}
}
}
// 由于 referent 要被释放了,因此 referent 的 weak_entry_t 也要移除出 weak_table
weak_entry_remove(weak_table, entry);
}

该函数主要逻辑是遍历所有弱引用该对象的 weak 指针,并将 weak 指针置为 nil,这也就是 weak 指针在对象释放的时候会被置为 nil 的原因。

以上是对象释放后,清理强弱引用相关逻辑,上面 objc_destructInstance 函数内部还有个 C++ 析构函数的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *objc_destructInstance(id obj) 
{
if (obj) {
// c++ 析构函数
bool cxx = obj->hasCxxDtor();

// 如果有 c++ 析构函数 则调用 c++ 析构函数.
if (cxx)
object_cxxDestruct(obj);

// 清理管理对象...
// 清理引用计数...
}

return obj;
}

那调用的析构函数 object_cxxDestruct 是什么呢?内部有哪些逻辑呢?这里不再深究,直接给出总结:
object_cxxDestruct 内部最终会调用 .cxx_destruct 的函数,《Effective Objective-C 2.0》提到过这个函数:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

根据介绍,.cxx_destruct 是编译器自动生成的,ARC 环境下,会借用这个函数实现自动内存释放的工作。

其实,.cxx_destruct 是在 clang 编译期间生成的,只有当前类拥有实例变量(不论是不是用 property)时,这个函数才会自动生成,且父类的实例变量不会导致子类拥有这个函数,其内部最终会调用 emitCXXDestructMethod 函数:

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
static void emitCXXDestructMethod(CodeGenFunction &CGF, ObjCImplementationDecl *impl)
{
CodeGenFunction::RunCleanupsScope scope(CGF);

llvm::Value *self = CGF.LoadObjCSelf();

const ObjCInterfaceDecl *iface = impl->getClassInterface();
for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin(); ivar; ivar = ivar->getNextIvar())
{
QualType type = ivar->getType();

// Check whether the ivar is a destructible type.
QualType::DestructionKind dtorKind = type.isDestructedType();
if (!dtorKind) continue;

CodeGenFunction::Destroyer *destroyer = 0;

// Use a call to objc_storeStrong to destroy strong ivars, for the
// general benefit of the tools.
if (dtorKind == QualType::DK_objc_strong_lifetime) {
destroyer = destroyARCStrongWithStore;

// Otherwise use the default for the destruction kind.
} else {
destroyer = CGF.getDestroyer(dtorKind);
}

CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
cleanupKind & EHCleanup);
}

assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
}

根据上述逻辑可以发现,该函数作用是遍历当前对象所有的实例变量(Ivars),调用 objc_storeStrong 函数:

1
2
3
4
5
6
7
id objc_storeStrong(id *object, id value) {
value = [value retain];
id oldValue = *object;
*object = value;
[oldValue release];
return value;
}

可以看到调用过 objc_storeStrong 函数后,这个实例变量就被 release 和设置成 nil 了。