Objective-C 小记(7)retain & re
本文使用的 runtime 版本为 objc4-706。
retain
retain
在现在的 runtime 中的默认实现是 objc_object
中的 retain
函数,可以在 objc-object.h
中找到它:
// Equivalent to calling [this retain], with shortcuts if there is no override
inline id
objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
retain
函数首先断言对象指针不是一个 tagged pointer(assert(!isTaggedPointer())
),之后对 isa
中是否有自定义 retain
和 release
实现标示位进行判断,如果没有自定义的实现,则进入默认实现 rootRetain
函数,否则的话直接向对象发送 retain
消息,调用自定义的 retain
实现。
本文的关注点当然是在默认实现上,所以继续查看 rootRetain
函数的实现:
ALWAYS_INLINE id
objc_object::rootRetain()
{
return rootRetain(false, false);
}
rootRetain
函数的实现是调用了另一个重载的 rootRetain
。
在继续对下面的代码进行分析之前,先回顾一下 isa
的结构(这里只对 x86-64 架构的 isa_t
进行分析):
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
};
在《Objective-C 小记(2)对象 2.0》中有对 isa_t
更详细的描述。现在需要关心的是 has_sidetable_rc
和 extra_rc
这两个位字段(bit-field)。extra_rc
表示「额外的 retain count」,假如 extra_rc
的值为 2,则对象的引用计数为 3。回顾《Objective-C 小记(6)alloc & init》可以发现,对象在创建时 extra_rc
的值是 0,引用计数则是 1。还可以注意到 extra_rc
只有 8 位,这样它最多能记到 255,如果这个时候引用计数还要往上增加怎么办呢?这时候对象会将一半的引用计数存储到一个表里,并将 has_sidetable_rc
置为 1。
回到 rootRetain
函数:
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
函数一开始又检查了自己是不是 tagged pointer(if (isTaggedPointer()) return (id)this;
),这难道就是防御式编程?
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
它首先声明了四个变量,四个变量都能从名字知道它们的用处:
-
sideTableLocked
,用来表示 side table 是否锁上了 -
transcribeToSideTable
,用来表示是否需要将isa
中的引用计数转移到 side table 里去 -
oldisa
,isa
本来的值 -
newisa
,isa
新的值(增加了引用计数后的值)
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
之后进入 do-while 循环,循环里首先将 transcribeToSideTable
赋值为 false
,oldisa
和 newisa
赋值为 isa.bits
的值(LoadExclusive
的作用是让读取操作原子化,根据 CPU 不同实现不同,比如在 x86-64 上就是单纯的直接返回值,而在 arm64 上则使用了 ldxr
指令)。
关于
slowpath
和fastpath
宏,在《Objective-C 小记(6)alloc & init》中有解释。关于
tryRetain
,这个参数与 weak 的实现有关,本文暂不做分析。
首先会检查 isa
是不是 non-pointer(if (slowpath(!newisa.nonpointer)) { ... }
),如果不是 non-pointer,就进入 sidetable_retain
这个过程,这是完全由一个表来存放引用计数的实现。
第二个判断则是和 tryRetain
有关,暂时不做分析。可以发现这两个判断使用的都是 slowpath
,表示是不太可能出现的情况。
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
接下来就是重点部分了,声明 carry
变量来标示是否溢出。然后使用 addc(newisa.bits, RC_ONE, 0, &carry)
给 newisa
的 extra_rc
位字段加 1。这里有个判断是否溢出,如果溢出的话还要判断 handleOverflow
是否为 true
,可以注意到这个函数被调用时 hadleOverflow
是 false
,需要进入 rootRetain_overflow
函数,而 rootRetain_overflow
的实现是这样的:
NEVER_INLINE id
objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}
它又重新调用 rootRetain
,不过将 handleOverflow
置为了 true
,希望有大神分享一下为什么要这样做……rootRetain
里剩余的工作也很好理解,将 side table 锁住,给 sideTableLocked
和 transcribeToSideTable
设置好值,extra_rc
留下一半(在 x86-64 下就是 126)的引用计数,并将 has_sidetable_rc
设置为 true
。
最后 while
里的操作是对比 isa
和 oldisa
的值,如果一样则将 newisa
覆盖 isa
,否则需要重新操作。
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
最后,函数检查 transcribeToSideTable
,也就是如果之前的操作有溢出,则将一半的引用计数加到表里。
release
release
的实现也在 objc-object.h
中:
// Equivalent to calling [this release], with shortcuts if there is no override
inline void
objc_object::release()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
和 retain
基本上是一致的,如果有自定义实现的话,则发消息,否则进入默认实现 rootRelease
:
ALWAYS_INLINE bool
objc_object::rootRelease()
{
return rootRelease(true, false);
}
套路真是一模一样,继续看 rootRelease
的实现:
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
开头也是一样的套路,不解释了。
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
同样也是进入一个 do-while 循环,套路满满,这里也不解释了。
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
使用 subc(newisa.bits, RC_ONE, 0, &carry)
给 newisa
的引用计数减 1,发现下溢出后跳转到 underflow
。如果没有溢出,函数就这样结束了。继续看 underflow
的代码:
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
首先将 newisa
重制,然后判断这个对象有没有 side table,有的话,可以把 side table 里的引用计数移过来。但判断里面又是判断 handleUnderflow
这个参数,rootRelease_underflow
的实现也是和 rootRetain_overflow
差不多的:
NEVER_INLINE bool
objc_object::rootRelease_underflow(bool performDealloc)
{
return rootRelease(performDealloc, true);
}
总之调用了这个函数还是会回到上面的代码,就继续往下看吧:
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}
首先将 side table 锁住,为了防止出现竞争又跑一遍 retry
。
// Try to remove some retain counts from the side table.
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.
if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
newisa.extra_rc = borrowed - 1; // redo the original decrement too
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_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
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.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
这里从 side table 借 RC_HALF
的引用计数放到 extra_rc
上。接下来的代码是从 side table 借不到的情况,那当然就是对象需要被销毁了。
// Really deallocate.
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__sync_synchronize();
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
}
可以看到,就是直接就发送了 dealloc
消息。
总结
对于现在的 non-pointer isa 来说,引用计数一部分存储在 isa 的 extra_rc
上,溢出后转移到一个表里。感觉是个很有意思的实现。