Reference Count - ShenYj/ShenYj.github.io GitHub Wiki
iOS 中 Object-C 与 Swift 都是引用计数的方式来管理内存,在实现上存在一些区别
暂时不考虑 weak 引用和 TaggedPointer 小对象这种特殊场景情况
目前网上看到的资料基本出自小码哥李明杰和逻辑教育的 OC 底层课程,这里我将结合 objc-781 和暂时最新的 objc-818.2 进行对比学习、总结
-
Object-C经过优化后的isa,首先会直接存储在isa的extra_rc这块19个二进制位空间中 (存储的值为引用计数-1) -
由于存储空间有限,就有可能会超出,因此在
isa中额外有一个标记位has_sidetable_rc -
当
has_sidetable_rc为1,时,引用计数就会被存储在一个叫SideTable的类的属性中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."); } void lock() { slock.lock(); } void unlock() { slock.unlock(); } void forceReset() { slock.forceReset(); } // Address-ordered lock discipline for a pair of side tables. template<HaveOld, HaveNew> static void lockTwo(SideTable *lock1, SideTable *lock2); template<HaveOld, HaveNew> static void unlockTwo(SideTable *lock1, SideTable *lock2); };
这里的
slock不要看类型就认为是自旋锁,通过818.2源码可见,是互斥锁的别名typedef mutex_t spinlock_t;
RefcountMap refcnts;就是用来存储引用计数的,是一个散列表的结构 -
非优化后的
isa, 直接存储在SideTable中关于
isa的结构布局,参考笔记 isa
在调用 retainCount 方法时,内部会调用到 _objc_rootRetainCount 函数
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}紧接着调用到 rootRetainCount 函数
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);
return obj->rootRetainCount();
}这里会通过 SUPPORT_NONPOINTER_ISA 标记判断是否是优化后的 isa 执行不同的函数,由于目前是优化后的 isa, 所以 rootRetainCount 函数的具体实现是
// 781 源码
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
// 818.2 源码
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
// 这行代码与上面的处理没有区别, LoadExclusive(&isa.bits) 函数也是直接调用了 _c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED) 返回
isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
if (bits.nonpointer) {
uintptr_t rc = bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}// isa 优化前直接从 SideTable 取计数值的方法 (781 和 818.2下无变化)
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
// isa 优化后 从 SideTable 取计数值的方法 (781 和 818.2下无变化)
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;
}获取引用计数函数调用过程解读
- 判断是
TaggedPointer的直接返回这里也是为什么某些情况下我们打印
NSString类型的引用计数为-1的原因,因为TaggedPointer指针足够存储当时的字符串,并不会进行引用计数管理 - 取出
isa.bits64位数据,判断是否是优化后指针类型,如果是优化后的指针,取出extra_rc2.1 如果has_sidetable_rc标记为为1,说明有通过SideTable额外存储,再取一下SideTable的值,加上extra_rc中的值,就是最终的引用计数值 2.2 如果has_sidetable_rc标记为为0,就直接返回extra_rc里面的计数值就可以了 - 如果是非优化后的指针(早期版本),是通过
sidetable_retainCount函数直接返回计数值
变化
- 最新的
818.2源码与781源码有所调整,早期文章提到extra_rc里面存储的是引用计数值 - 1,因此在获取计数值的时候,会进行+1(uintptr_t rc = 1 + bits.extra_rc;);但从818.2源码可见,已经不再+1
-
函数的调用过程:
-
781- (void)release->_objc_rootRelease(self)->obj->rootRelease()->rootRelease(false, false)最终来到
ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow)函数-
rootRelease718 源码rootRelease (718 源码)
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 (rawISA()->isMetaClass()) return false; if (sideTableLocked) sidetable_unlock(); return sidetable_release(performDealloc); } // 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; 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); } // 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; } // 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. } } // 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(); __c11_atomic_thread_fence(__ATOMIC_ACQUIRE); if (performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc)); } return true; }
-
-
818.2- (void)release->_objc_rootRelease(self)->obj->rootRelease()->rootRelease(true, RRVariant::Fast)最终来到
ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)函数-
rootRelease818.2 源码, 代码比较长,有多处变化rootRelease (818.2源码)
LWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant) { if (slowpath(isTaggedPointer())) return false; bool sideTableLocked = false; isa_t newisa, oldisa; oldisa = LoadExclusive(&isa.bits); if (variant == RRVariant::FastOrMsgSend) { // These checks are only meaningful for objc_release() // They are here so that we avoid a re-load of the isa. if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) { ClearExclusive(&isa.bits); if (oldisa.getDecodedClass(false)->canCallSwiftRR()) { swiftRelease.load(memory_order_relaxed)((id)this); return true; } ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release)); return true; } } if (slowpath(!oldisa.nonpointer)) { // a Class is a Class forever, so we can perform this check once // outside of the CAS loop if (oldisa.getDecodedClass(false)->isMetaClass()) { ClearExclusive(&isa.bits); return false; } } retry: do { newisa = oldisa; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); return sidetable_release(sideTableLocked, performDealloc); } if (slowpath(newisa.isDeallocating())) { ClearExclusive(&isa.bits); if (sideTableLocked) { ASSERT(variant == RRVariant::Full); sidetable_unlock(); } return false; } // 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(newisa.isDeallocating())) goto deallocate; if (variant == RRVariant::Full) { if (slowpath(sideTableLocked)) sidetable_unlock(); } else { ASSERT(!sideTableLocked); } return false; 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 (variant != RRVariant::Full) { ClearExclusive(&isa.bits); return rootRelease_underflow(performDealloc); } // 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. oldisa = LoadExclusive(&isa.bits); goto retry; } // Try to remove some retain counts from the side table. auto borrow = sidetable_subExtraRC_nolock(RC_HALF); bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there if (borrow.borrowed > 0) { // Side table retain count decreased. // Try to add them to the inline count. bool didTransitionToDeallocating = false; newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too newisa.has_sidetable_rc = !emptySideTable; bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits); if (!stored && oldisa.nonpointer) { // 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. uintptr_t overflow; newisa.bits = addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow); newisa.has_sidetable_rc = !emptySideTable; if (!overflow) { stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits); if (stored) { didTransitionToDeallocating = newisa.isDeallocating(); } } } if (!stored) { // Inline update failed. // Put the retains back in the side table. ClearExclusive(&isa.bits); sidetable_addExtraRC_nolock(borrow.borrowed); oldisa = LoadExclusive(&isa.bits); goto retry; } // Decrement successful after borrowing from side table. if (emptySideTable) sidetable_clearExtraRC_nolock(); if (!didTransitionToDeallocating) { if (slowpath(sideTableLocked)) sidetable_unlock(); return false; } } else { // Side table is empty after all. Fall-through to the dealloc path. } } deallocate: // Really deallocate. ASSERT(newisa.isDeallocating()); ASSERT(isa.isDeallocating()); if (slowpath(sideTableLocked)) sidetable_unlock(); __c11_atomic_thread_fence(__ATOMIC_ACQUIRE); if (performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc)); } return true; }
-
-
部分解读 (代码逻辑较多,
818.2增加了不少代码 )
-
TaggedPointer直接返回 -
根据缓存的计数值做
-1操作
2.1 如果不是优化后的isa直接SideTable散列表-1
2.2 如果是优化后的isa,则对extra_rc中的引用计数值进行-1- 通过
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--这样代码可以判断出在做-1操作 - 当
extra_rc为0后,并且has_sidetable_rc标记了有额外的存储计数size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);取出一半-1后给extra_rc
- 通过
-
如果最终
-1后为0,performDealloc条件成立,通过msg_send执行dealloc
-
函数的调用过程:
-
781-(id) retain->_objc_rootRetain(self)->obj->rootRetain()->rootRetain(false, false)最终来到
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow)函数-
rootRetain718 源码rootRetain (718源码)
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { if (isTaggedPointer()) return (id)this; bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; do { transcribeToSideTable = false; oldisa = LoadExclusive(&isa.bits); newisa = oldisa; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); if (rawISA()->isMetaClass()) return (id)this; 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; } 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))); 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; }
-
-
818.2-(id) retain->_objc_rootRetain(self)->obj->rootRetain()->rootRetain(false, RRVariant::Fast)最终来到
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)函数-
rootRetain818.2 源码rootRetain (818.2源码)
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant) { if (slowpath(isTaggedPointer())) return (id)this; bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; oldisa = LoadExclusive(&isa.bits); if (variant == RRVariant::FastOrMsgSend) { // These checks are only meaningful for objc_retain() // They are here so that we avoid a re-load of the isa. if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) { ClearExclusive(&isa.bits); if (oldisa.getDecodedClass(false)->canCallSwiftRR()) { return swiftRetain.load(memory_order_relaxed)((id)this); } return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain)); } } if (slowpath(!oldisa.nonpointer)) { // a Class is a Class forever, so we can perform this check once // outside of the CAS loop if (oldisa.getDecodedClass(false)->isMetaClass()) { ClearExclusive(&isa.bits); return (id)this; } } do { transcribeToSideTable = false; newisa = oldisa; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; else return sidetable_retain(sideTableLocked); } // don't check newisa.fast_rr; we already called any RR overrides if (slowpath(newisa.isDeallocating())) { ClearExclusive(&isa.bits); if (sideTableLocked) { ASSERT(variant == RRVariant::Full); sidetable_unlock(); } if (slowpath(tryRetain)) { return nil; } else { return (id)this; } } uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ if (slowpath(carry)) { // newisa.extra_rc++ overflowed if (variant != RRVariant::Full) { 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))); if (variant == RRVariant::Full) { 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(); } else { ASSERT(!transcribeToSideTable); ASSERT(!sideTableLocked); } return (id)this; }
-
-
代码调用过程与
release很相似, 之前是减操作,这里是加操作
-
在优化
isa以前,直接存在SideTable散列表中 -
在优化
isa以后,肯定优先存在extra_rc里 2.1 如果这里存满了,那么会取出一半存放到SideTable中去,并将has_sidetable_rc标记为1(slowpath(carry))条件成立时,就是extra_rc满了-
newisa.extra_rc = RC_HALF;和sidetable_addExtraRC_nolock(RC_HALF);分别是半劈存储 😁
这么操作的目的在于提高性能,因为如果都存在散列表中,当需要
release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表。性能会提高很多-
isa的bits为 8字节 = 64 bit,根据结构体位域内存分布,nonpointer是最低位,bits中的1ULL<<45(arm64)后就是extra_rc最低位,通过addc函数执行加法运算 (对比release通过subc实现-1运算),也就是要在extra_rc的存储空间最低位上去+1 -
__arm64__# define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL # define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18)
-
在了解 Swift 引用计数前,应该先了解下 Swift 类结构,可以先参考笔记 Swift实例对象内存结构 和 HeapObject
在之前简单探索 Swift 源码得知,非继承自 NSObject 的 Swift 类结构中专门有个 SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS 8字节来存放引用计数,这可比优化后 isa 指针那 19个二进制位 大得多了
通过 Swift 源码可以大概了解, Swift 有三种引用计数
@_silgen_name("swift_retainCount")
public func _getRetainCount(_ Value: AnyObject) -> UInt
@_silgen_name("swift_unownedRetainCount")
public func _getUnownedRetainCount(_ Value: AnyObject) -> UInt
@_silgen_name("swift_weakRetainCount")
public func _getWeakRetainCount(_ Value: AnyObject) -> UIntSwift 源码看的还不够深,下次得重新编译一个 Xcode 工程来阅读,目前用 VSCode 看起来很不舒服
TBD.