retain和release实现探究

Runtime

前面一篇文章,我们讲到了系统为了优化数字字符串等类型的数据存储新增了一种NSTaggedPointer类型,同时我们还发现,isa指针在经过优化后,提供了19个bit位用来存储引用计数的个数。但是如果超出了这个限制呢?

引用计数

其实在绝大多数情况下,仅用优化的isa_t来记录对象的引用计数就足够了,但是当对象被引用次数超过 2^19 限制时,就轮到SideTable出场了。

首先,我们先看下超出限制之后,系统是如何将引用计数转移的

retian

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 等价于直接使用对象调用retain方法
inline id
objc_object::retain()
{
// 如果是TaggedPointer类型 不涉及引用计数
ASSERT(!isTaggedPointer());
// fastpath 表示if中的条件是一个大概率事件
// 如果当前对象没有自定义(override)retain 方法
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
// 如果有自定义的retain方法
// 通过发消息的方式调用自定义的 retain 方法
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}

接下来我们主要看下 引用计数+1的主要负责函数rootRetain

rootRetain

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
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
// 如果如果是taggedPointer直接返回不需要引用计数
if (isTaggedPointer()) return (id)this;
// 默认不使用sideTable
bool sideTableLocked = false;
// 是否需要将引用计数转到sidetable
bool transcribeToSideTable = false;

// 记录新旧两个isa指针
isa_t oldisa;
isa_t newisa;

do {
transcribeToSideTable = false;
//// 通过 LoadExclusive 方法加载 isa 的值,加锁
oldisa = LoadExclusive(&isa.bits);
// 此时 newisa = oldisa
newisa = oldisa;
// slowpath表示if中的条件是小概率事件
// 如果newisa(此时和oldisa相等) 如果没有采用isa优化
if (slowpath(!newisa.nonpointer)) {
// 解锁
ClearExclusive(&isa.bits);
//rawISA() = (Class)isa.bits
// 如果当前对象的 isa 指向的类对象是元类(也就是说当前对象不是实例对象,而是类对象),直接返回
if (rawISA()->isMetaClass()) return (id)this;
// 如果不需要retain对象(引用计数+1) 且sideTable是锁上的
if (!tryRetain && sideTableLocked)
// sidetable解锁
sidetable_unlock();
if (tryRetain)
// sidetable_tryRetain 尝试对引用计数器进行+1的操作 返回+1操作是否成功
return sidetable_tryRetain() ? (id)this : nil;
else
// 将sidetable中保存的引用计数+1同时返回引用计数
return sidetable_retain();
}
// 如果需要尝试 +1 但是当前对象正在销毁中
if (slowpath(tryRetain && newisa.deallocating)) {
// 解锁
ClearExclusive(&isa.bits);
// 如果不需要去尝试 +1 并且 SideTables 表锁住了,就将其解锁
// 这里的条件 应该永远都不会被满足
if (!tryRetain && sideTableLocked)
sidetable_unlock();
// 如果对象正在被释放 执行retain是无效的
return nil;
}
// 引用计数是否溢出标志位
uintptr_t carry;
//为 isa 中的 extra_rc 位 +1 ,并保存引用计数
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
// 如果 isa中的extra_rc 溢出
if (slowpath(carry)) {
// newisa.extra_rc++ 溢出
// 是否需要处理溢出 这个变量是rootRetain函数外部传入的参数 是否需要处理溢出时的情况
if (!handleOverflow) {
//解锁
ClearExclusive(&isa.bits);
// rootRetain_overflow 方法实际上就是递归调用了当前方法只是将handleOverflow
// 置为yes
return rootRetain_overflow(tryRetain);
}
// 保留isa中extra_rc一半的值 将另一半转移到sidetable中
// 如果不需要尝试 +1 并且 sidetable 表未加锁,就将其加锁
if (!tryRetain && !sideTableLocked) sidetable_lock();
// sidetable加锁
sideTableLocked = true;
// 需要将引用计数转移到sidetable
transcribeToSideTable = true;
// 将newisa中的引用计数置为之前的一半 # define RC_HALF (1ULL<<18)
newisa.extra_rc = RC_HALF;
// isa中是否使用sidetable存储retiancount的标志位置为1
newisa.has_sidetable_rc = true;
}
//while循环开始 直到 isa.bits 中的值被成功更新成 newisa.bits
// StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
// 将更新后的newisa的值更新到isabit中
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

// 如果需要转移引用计数到sidetable中
if (slowpath(transcribeToSideTable)) {
// 将溢出的引用计数加到 sidetable 中
sidetable_addExtraRC_nolock(RC_HALF);
}
// 如果不需要去尝试 +1 并且 SideTables 表锁住了,就将其解锁
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
// 返回当前对象 引用计数已完成+1操作
return (id)this;
}

tryRetain标志位为1时,我们会先尝试调用sidetable_tryRetain方法,我们先看下这个方法:

sidetable_tryRetain
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
//尝试将 SideTable 表中的引用计数 +1
bool
objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
// 根据对象地址获取到引用计数器所在的sideTable
SideTable& table = SideTables()[this];

// NO SPINLOCK HERE
// _objc_rootTryRetain() is called exclusively by _objc_loadWeak(),
// which already acquired the lock on our behalf.

// fixme can't do this efficiently with os_lock_handoff_s
// if (table.slock == 0) {
// _objc_fatal("Do not call -_tryRetain.");
// }

bool result = true;
// try_emplace 如果给定的key在容器中不存在,原位构造一个元素 如果在容器中则返回
// try_emplace 有两个返回值 第一个返回值是一个遍历器 第二个返回值表示 key value 在map中是否已存在
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE);
// 获取引用计数(含两个标志位)
auto &refcnt = it.first->second;

if (it.second) {
//如果这个对象对应的实体第一次插入到sidetable中
} else if (refcnt & SIDE_TABLE_DEALLOCATING) {
// 如果当前对象处于正在被销毁状态SIDE_TABLE_DEALLOCATING标志位为1
result = false;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
// 引用计数没有溢出 + 1
refcnt += SIDE_TABLE_RC_ONE;
}
// 返回值为引用计数是否成功+1
return result;
}

当前的对象没有处于正在被销毁的状态时,我们会将sidetable中的引用计数+1。

如果tryRetain标志位为0,那么我们直接调用sidetable_retain方法对引用计数器进行+1操作,sidetable_retain方法如下:

sidetable_retain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 将 SideTable 表中的引用计数 +1
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
// 根据对象获取 存储引用计数的sidetable
SideTable& table = SideTables()[this];

table.lock();
// 获取sidetable中存储的引用计数值
size_t& refcntStorage = table.refcnts[this];
// 如果引用计数值没有溢出
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
// 引用计数值+SIDE_TABLE_RC_ONE
// #define SIDE_TABLE_RC_ONE (1UL<<2)
// SIDE_TABLE_RC_ONE = 4 为什么这里会+4我们下面会介绍
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();

return (id)this;
}

通过上面这个方法,我们对引用计数器完成了+1(实际上是+4)的操作,那么这里为什么会+4呢?
那是因为对于table.refcnts,实际上并不完全是表示引用计数的值,refcnts的最后两位有特殊的标示意义:

1
2
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1)
  • 倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向);
  • 倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态);

因此,我们每次执行retain方法时,虽然每次都是+4,但是对于引用计数真实的值来说就是+1,64位环境下只有62位是保存溢出的引用计数的.

紧接如果对象没有采用isa优化且对象没有正在销毁,我们通过调用addc方法实现引用计数器+1的操作,这个方法会给我们一个标志值carry,表示进行+1操作后,引用计数是否溢出。

如果发生溢出,但是此时我们不需要处理溢出:

那么我们会直接调用rootRetain_overflow方法,我们先来看下这个方法:

rootRetain_overflow
1
2
3
4
5
NEVER_INLINE id 
objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}

很明显 这个方法实际是递归调用了rootRetain方法,只是handleOverflow参数值被置为yes。而对于retain操作来说实际上是走出了刚才if (!handleOverflow)判断。那么我们继续往下看。

如果发生溢出,且我么需要处理溢出时:
我们需要先设置标志位:

  • sideTableLocked = true;
  • transcribeToSideTable = true;
  • newisa.extra_rc = RC_HALF;
  • newisa.has_sidetable_rc = true;

同时将newisa的值更新到isa中,保存成功后,while循环结束。

紧接着我们调用sidetable_addExtraRC_nolock方法,下面我们再来看下这个方法:

1
2
3
4
if (slowpath(transcribeToSideTable)) {
//拷贝 平外一半的 引用计数到 side table
sidetable_addExtraRC_nolock(RC_HALF);
}

RC_HALF的定义如下

1
#define RC_HALF  (1ULL<<18)

我们都知道NSTaggedPointer预留了19个bit位用来存放引用计数,RC_HALF的值刚好为 2^19 次方的一半。

我们下面来看下sidetable_addExtraRC_nolock如何实现的

sidetable_addExtraRC_nolock
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

// Move some retain counts to the side table from the isa field.
// Returns true if the object is now pinned.
// 将isa中的引用计数移动到sidetable中 当引用计数达到最大值(溢出)是返回true
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
ASSERT(isa.nonpointer);
// 根据对象地址获取到存放引用计数对应的sidetable
SideTable& table = SideTables()[this];
// 从table.refcnts中获取当前对象的引用计数
size_t& refcntStorage = table.refcnts[this];
// 声明一个局部变量存储旧的引用计数
size_t oldRefcnt = refcntStorage;
// isa-side bits should not be set here
// 如果就的引用计数>0或sidetable正在销毁
// 如果引用计数>0或当前对象再被其他对象弱引用
ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
// #define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
// oldRefcnt & SIDE_TABLE_RC_PINNED = 1 就是 oldRefcnt = 2147483648 (32位情况)
// 这时候 引用计数已经超过了三十二位所能表达的最大值 直接返回true
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

// 溢出标志位
uintptr_t carry;
// 对oldRefcnt执行+delta_rc操作
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
// 如果引用计数溢出 设置标识为已满
if (carry) {
// 如果是32位的情况 SIDE_TABLE_RC_PINNED = 1<< (32-1)
// int的最大值 SIDE_TABLE_RC_PINNED = 2147483648
// SIDE_TABLE_FLAG_MASK = 3
// refcntStorage = 2147483648 | (oldRefcnt & 3)
// 如果溢出,直接把refcntStorage 设置成最大值
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
// 如果没有溢出 那么直接将新的引用计数赋值给refcntStorage
refcntStorage = newRefcnt;
return false;
}
}

向右偏移两位的原因是,RefcountMap refcnts的最后两位有特殊的标示意义:

1
2
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1)
  • 倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向);
  • 倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态);

所以,64位环境下只有62位是保存溢出的引用计数的.

通过上面的介绍我们了解到了引用计数是如何在sidetable中存储的(retian方法)。那么引用计数-1的操作又是怎么实现的呢?

release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Equivalent to calling [this release], with shortcuts if there is no override
// 等价于直接使用对象调用release方法
inline void
objc_object::release()
{
ASSERT(!isTaggedPointer());
// 如果没有自定义的release方法 就直接调用rootRelease
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
// 如果有自定义的release方法那么调用对象的release方法
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}

从上面的代码我们看到,在没有自定义release方法时,系统默认是调用的rootRelease方法,下面我们来看下这个方法。

rootRelease

1
2
3
4
5
6
ALWAYS_INLINE bool 
objc_object::rootRelease()
{
// 调用了私有函数 rootRelease 去实现
return rootRelease(true, false);
}

上面这个方法实际上调用了同名函数(两个默认参数),下面我们进一步看下带有两个参数的rootRelease方法:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// 真正的release方法
// 两个参数分别是 是否需要调用dealloc函数,是否需要处理 向下溢出的问题
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
// 如果是TaggedPointer 不需要进行release操作
if (isTaggedPointer()) return false;
// 局部变量sideTable是否上锁 默认false
bool sideTableLocked = false;

// 两个局部变量用来记录这个对象的isa指针
isa_t oldisa;
isa_t newisa;

retry:
do {
// 加载这个isa指针
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// 如果没有进行nonpointer优化
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
// 如果是类对象直接返回false 不需要释放
if (rawISA()->isMetaClass()) return false;
// 如果sideTableLocked 则解锁 这里默认是false
if (sideTableLocked)
sidetable_unlock();
// 调用sidetable_release 进行引用计数-1操作
return sidetable_release(performDealloc);
}

// 溢出标记位
uintptr_t carry;
// newisa 对象的extra_rc 进行-1操作
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
// 如果-1操作后 向下溢出了 结果为负数
if (slowpath(carry)) {
// don't ClearExclusive()
// 调用underflow 进行向下溢出的处理
goto underflow;
}
// 开启循环,直到 isa.bits 中的值被成功更新成 newisa.bits
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));

//走到这说明引用计数的 -1 操作已完成
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;

underflow:
//newisa的extra_rc在执行-1操作后导致了向下溢出
// 放弃对newisa的修改 使用之前的oldisa
newisa = oldisa;

// 如果 isa 的 has_sidetable_rc 标志位标识引用计数已溢出
// has_sidetable_rc 用于标识是否当前的引用计数过大,无法在isa中存储,
// 而需要借用sidetable来存储。(这种情况大多不会发生)
if (slowpath(newisa.has_sidetable_rc)) {
// 是否需要处理下溢
if (!handleUnderflow) {
// 清除原 isa 中的数据的原子独占
ClearExclusive(&isa.bits);
// 如果不需要处理下溢 直接调用 rootRelease_underflow方法
return rootRelease_underflow(performDealloc);
}

// 如果sidetable是上锁状态
if (!sideTableLocked) {
// 解除清除原 isa 中的数据的原子独占
ClearExclusive(&isa.bits);
// sidetable 上锁
sidetable_lock();
sideTableLocked = true;
// 跳转到 retry 重新开始,避免 isa 从 nonpointer 类型转换成原始类型导致的问题
goto retry;
}

// sidetable_subExtraRC_nolock 放回要从sidetable移动到isa的extra_rc的值
// 默认是获取extra_rc可存储的长度一半的值
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.
// 为了避免冲突 has_sidetable_rc 标志位必须保留1的状态 及时sidetable中的个数为0
if (borrowed > 0) {
// 将newisa中引用计数值extra_rc 设置为borrowed - 1
// -1 是因为 本身这次是release操作
newisa.extra_rc = borrowed - 1;
// 然后将修改同步到isa中
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
// 如果保存失败
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
// 从新装载isa
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
// 如果newisa2是nonpointer类型
if (newisa2.nonpointer) {
// 下溢出标志位
uintptr_t overflow;
// 将从 SideTables 表中获取的引用计数保存到 newisa2 的 extra_rc 标志位中
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
//
if (!overflow) {
// 如果没有溢出再次将 isa.bits 中的值更新为 newisa2.bits
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}

// 如果重试之后依然失败
if (!stored) {
// 将从sidetable中取出的引用计数borrowed 重新加到sidetable中
sidetable_addExtraRC_nolock(borrowed);
// 重新尝试
goto retry;
}

// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
// 完成对 SideTables 表中数据的操作后,为其解锁
sidetable_unlock();
return false;
}
else {
// 在从Side table拿出一部分引用计数之后 Side table为空
// Side table is empty after all. Fall-through to the dealloc path.
}
}

// 如果当前的对象正在被释放
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
// 如果sideTableLocked被锁 那么解锁
if (sideTableLocked) sidetable_unlock();
// 兑现被过度释放
return overrelease_error();
// does not actually return
}
// 将对象被释放的标志位置为true
newisa.deallocating = true;
// 将newisa同步到isa中 如果失败 进行重试
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))
goto retry;

// 如果sideTableLocked= true
if (slowpath(sideTableLocked))
// Side table解锁
sidetable_unlock();

__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

// 如果需要执行dealloc方法 那么调用该对象的dealloc方法
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}

这个方法还是有点长的,我们看到主要是由两个内部方法retry,underflow组成,下面我们来一步步的整理下引用计数-1操作的具体步骤

retry

sidetable_release

如果这个对象没有nonpointer优化,且不是一个类对象,那么我们直接通过对sidetable进行-1操作

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
// 将 SideTable 表中的引用计数 -1
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
// 根据对象地址获取SideTable
SideTable& table = SideTables()[this];
// 是否需要执行dealloc方法 默认是false
bool do_dealloc = false;

table.lock();
// 获取当前对象的销毁状态 方法的返回值有2个
// 引用计数和当前对象是否已存在与map中
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
auto &refcnt = it.first->second;
// 如果当前对象之前不存在与map中
if (it.second) {
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
// 如果引用计数的值小于 SIDE_TABLE_DEALLOCATING = 2(0010)
// refcnt 低两位分别是SIDE_TABLE_WEAKLY_REFERENCED 0 SIDE_TABLE_DEALLOCATING 1
// 这个对象需要被销毁
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
// 如果引用计数有值且未溢出那么-1
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
// 如果需要执行dealloc 那么就调用这个对象的dealloc
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}

相反,如果该对象做了nonpointer优化,那么我们直接对extra_rc进行-1操作,即

1
2
3
4
5
6
7
8
// newisa 对象的extra_rc 进行-1操作
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
// 如果-1操作后 向下溢出了 结果为负数
if (slowpath(carry)) {
// don't ClearExclusive()
// 调用underflow 进行向下溢出的处理
goto underflow;
}

将extra_rc计数-1,如果发现-1操作之后,extra_rc的个数为0,那么就出现了向下溢出,我们需要将sideTable中的部分引用计数拿到extra_rc中记录。如果没有向下溢出,那么我们就直接将修改后的newisa同步到isa中即完成了release操作。

underflow

如果在将extra_rc进行-1操作时,出现了向下溢出的问题,那么我们需要将sideTable中的引用计数移动到extra_rc中存储。

下面我们来分析下具体过程

先判断has_sidetable_rc是否有sidetable引用计数,如果有我们要确认是否需要处理向下溢出,如果不需要处理向下溢出,那么我们直接调用rootRelease_underflow方法,

rootRelease_underflow
1
2
3
4
5
NEVER_INLINE uintptr_t
objc_object::rootRelease_underflow(bool performDealloc)
{
return rootRelease(performDealloc, true);
}

很明显这个方法实际上与retain操作时处理溢出逻辑相同,将rootRelease方法中的handleUnderflow参数置为true,要处理向下溢出。

下面我们再来看下,需要处理向下溢出时,如果当前的sidetable处于未上锁的状态时,将sidetable上锁然后进行重试,如果sidetable未已经上锁了,那么我们会执行下面这句代码:

1
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

sidetable_subExtraRC_nolock 返回要从sidetable移动到isa的extra_rc的值,默认是获取extra_rc可存储的长度一半的值。

如果此时从sidetable中拿到的值 > 0,那么我们要将这部分值放到isa的extra_rc中进行存储,如果取到的borrowed的值为0,那么说明sidetable中的引用计数为0,那么我们直接释放该对象即可。

StoreReleaseExclusive

上面说到如果从sidetable中获取到的值borrowed大于0,那么我们直接将newisa.extra_rc设置为borrowed - 1即可。

然后我们在调用StoreReleaseExclusive方法将newisa同步到isa中。

如果这里StoreReleaseExclusive方法保存失败了,那么我们需要重新调用LoadExclusive重新声明两个变量newisa2,oldisa2。通过addc方法将extra_rc置为borrowed-1

1
newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);

然后再次调用StoreReleaseExclusive方法将newisa2的改动同步到isa中。

如果StoreReleaseExclusive方法依然保存失败,那么我们就把从sidetable中获取的borrowed重新加到sideTable中。然后调用retry方法。

经过StoreReleaseExclusive这一步,引用计数更新操作完成。但是如果此时的引用计数为0我们改如何操作呢?

如果引用计数更新成功,那么我们需要先判断,当前对象是否正在被释放,如果正在被释放 那么调用过度释放方法overrelease_error

overrelease_error
1
2
3
4
5
6
7
NEVER_INLINE uintptr_t
objc_object::overrelease_error()
{
_objc_inform_now_and_on_crash("%s object %p overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug", object_getClassName((id)this), this);
objc_overrelease_during_dealloc_error();
return 0; // allow rootRelease() to tail-call this
}

这个方法主要是定义了crash信息,当一个用户正在被释放时,再次调用release方法时会导致crash,具体crash信息如上述代码。

如果当前对象没有被正在释放,那么我们将当前对象正在被释放标志位置为true newisa.deallocating = true; 同时将状态的更新同步到isa中。如果同步失败,那么会重复走一次retry。

更新状态成功后,对sidetable的操作也结束了,我们就可以将sidetable解锁(sidetable_unlock),如果需要执行dealloc方法,那么我们调用dealloc方法进行对象释放通知。

总结

至此我们就看完了objc对于retain和release以及其对引用计数的操作,以及在retain操作时当extra_rc空间不足时,引用计数是如何从extra_rc转移到sidetable中和release操作时引用计数是如何从sidetable转移到extra_rc中的。希望看了这篇文章可以帮你更好的了解引用计数的实现。