前言
在iOS中,对象的内存是通过引用计数(Reference Count
)来管理的。每当有一个新的强引用指针指向,对象的引用计数就会+1
,当减少一个强引用指针,引用计数就会-1
,当引用计数为0
时,对象就会被销毁。
一、引用计数值的存储
在前面介绍alloc核心步骤的相关文章中有提到,一个nonpointer
类型的对象,它的引用计数是存放在成员isa_t
里的extra_rc
中。在__arm64__
环境下,extra_rc
在内存中占19位,在__x86_64__
环境下,占8位。
isa_t
结构里和内存管理相关的成员除extra_rc
外,还有weakly_referenced
、has_sidetable_rc
以及deallocating
这三个,具体情况可参考iOS原理 alloc核心步骤3:initInstanceIsa详解。
以__x86_64__
环境为例,extra_rc
大小总共8bit
,最多存放2^7
量级的数值。因此如果只用extra_rc
来存储引用计数值,就会遇到下面3个问题:
- 若
extra_rc
达到最大值,此时对象又被一个新的强引用指针指向,该如何处理? - 若对象不是
nonpointer
类型,则isa_t
结构里就没有extra_rc
成员,此时引用计数该如何处理? - 若对象被一个弱引用指针指向,该如何处理?
基于此,除了extra_rc
外,OC中还使用了SideTables
散列表来管理引用计数。
二、SideTables 散列表
SideTables
是一个全局的哈希数组,里面存储了多张SideTable
。本质是一个StripedMap
结构体,内部成员StripeCount
表示SideTable
的最大数量:
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
可以看到,在iOS的真机模式下,SideTable
最多为8张,在MacOS或者模拟器模式下,最多为64张。
SideTables
的哈希key就是对象的地址,每个地址都会映射一张SideTable
,由于最大数量限制,因此会有很多对象地址映射同一张SideTable
。通过对哈希函数传入对象地址,即可得到对应的SideTable
。
2.1 SideTable
SideTable
里面主要存放了对象的引用计数和弱引用相关信息,结构如下:
struct SideTable {
//成员
spinlock_t slock; //自旋锁,防止多线程访问冲突
RefcountMap refcnts; //引用计数表
weak_table_t weak_table; //弱引用表
//函数
... ...
};
内部有三个成员:
-
spinlock_t slock
: 自旋锁,用于上锁/解锁SideTable
spinlock_t
实质上是一个uint32_t
类型的非公平的自旋锁(unfair lock)
,当值大于0时,锁可用,当等于或小于0时,需要锁等待。在_os_unfair_lock_opaque
中记录了获取锁的线程信息,只有获得该锁的线程才能够解开这把锁。非公平:指获取锁的顺序和申请锁的顺序无关。也就是说,第一个申请锁的线程有可能最后是最后一个获取锁,或者刚获得锁的线程也有可能会再次立刻获取锁,造成饥饿等待。
-
RefcountMap refcnts
:引用计数表,存储对象的引用计数RefcountMap
实质上是一个以objc_object
为key的hash表,value为对象的引用计数。RefcountMap
中可以存储多个对象的引用计数,因此一个SideTable
会对应多个对象。对于nonpointer
类型对象,当extra_rc
达到最大值后,才会在RefcountMap
中存放引用计数,而对于非nonpointer
类型对象,直接在里面存放引用计数。当对象的引用计数变为0时,会自动将相关的信息从hash表中删除。 -
weak_table_t weak_table
:弱引用表,存储对象的弱引用指针weak_table_t
也是一个以objc_object
为key的hash表,结构如下:struct weak_table_t { weak_entry_t *weak_entries; // hash数组,用来存储弱引用对象的相关信息 size_t num_entries; // hash数组中的元素个数 uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个) uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值) };
每个对象对应一个
weak_entry_t
,其结构如下:struct weak_entry_t { //被引用的对象 DisguisedPtr<objc_object> referent; //存储该对象的弱引用指针 //如果弱引用指针数量大于4,存放在referrers数组,小于4,存放在inline_referrers数组 union { struct { weak_referrer_t *referrers; // 存储弱引用指针地址的hash数组 uintptr_t out_of_line_ness : 2; uintptr_t num_refs : PTR_MINUS_2; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { // out_of_line_ness field is low bits of inline_referrers[1] weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; // 存储弱引用指针地址的hash数组 }; }; //函数 ... ... };
由此可知,对象的弱引用指针都存储在其对应的
weak_entry_t
里的数组中。当对象被一个新的弱引用指针指向时,就会往数组里添加这个指针,若指针指向nil或者其它对象,则将该指针从数组里移除,若对象的引用计数为0,则会将数组里的所有弱引用指针指向nil,再移除。
2.2 SideTable存在多张的原因
- 若全局只用一张
SideTable
来管理所有对象,每次访问一个对象都会进行一次开/解锁操作,访问其他对象需要等待,效率过低。 - 若为每个对象都建立一个
SideTable
,则会造成内存浪费,耗费性能。 - 至于为什么最多为8张或者64张,目前并没有什么数据模型和理论支撑,猜测是设计
SideTables
的作者根据经验选择的一个定值。
三、引用计数的底层处理
在MRC
中,需要程序员手动调用retain
方法来使引用计数+1,调用release
方法来使引用计数-1,当引用计数为0时,会调用dealloc
方法销毁。在ARC
中也一样,只不过不需要程序员手动调用,编译器会自动调用。
3.1 retain 源码分析
在源码中retain
操作的底层函数调用链为objc_retain
-> retain
-> rootRetain
,最终实现代码如下:
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//1.若对象为TaggedPointer对象,直接返回
if (isTaggedPointer()) return (id)this;
//声明两个标记
bool sideTableLocked = false; //sideTable是否被锁
bool transcribeToSideTable = false; //是否需要更新SideTable中的引用计数
//声明两个isa_t的局部变量,用于新旧值的替换
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
//这里的isa是对象自身的isa,并赋值给两个局部isa保存
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//2.若对象不是nonpointer类型,直接操作sidetable
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
//3.如果当前对象正在释放,执行dealloc流程
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
//4.若对象是nonpointer类型的对象,则将extra_rc值+1
//先通过左移运算获取到isa里的extra_rc,+1后再将新值赋给isa
//carry标记,用来表示extra_rc的值是否已溢出
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判断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();
//若溢出,则需要SideTable来储存
//更新上面声明的SideTable的两个标记
sideTableLocked = true;
transcribeToSideTable = true;
//将(extra_rc最大值 + 1)的一半存储在extra_rc中
//在__x86_64__下,extra_rc占8位,RC_HALF为1<<7,所以是(最大值 + 1)的一半
newisa.extra_rc = RC_HALF;
//将isa中的has_sidetable_rc值设为1,表示该对象已经使用Sidetable来存储引用计数了
newisa.has_sidetable_rc = true;
}
//这个while判断条件里面已经将newisa赋值给对象的isa了
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
//4.判断是否需要更新SideTable里的引用计数
//只在extra_rc达到最大值时,才需要更新
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//将(extra_rc最大值 + 1)的1/2存储在Sidetable中
sidetable_addExtraRC_nolock(RC_HALF);
}
//解锁SideTable
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
经过源码分析可知,retain
的实现逻辑如下:
-
Step1
:如果对象为TaggedPointer
对象,则直接返回,不做其他操作。 -
Step2
:如果对象不是nonpointer
类型,若对象为元类对象,则直接返回,若不是元类对象,则直接操作SideTable
,将对象的引用计数+1并保存在RefcountMap
中。 -
Step3
:如果对象是否正在释放,则执行dealloc流程。 -
Step4
:如果对象是nonpointer
类型,执行extra_rc+1
,并判断extra_rc
是否溢出。若已溢出,则将(extra_rc最大值 + 1)
的一半分别存在isa
的extra_rc
和SideTable
的RefcountMap
中。
为什么是
(extra_rc最大值 + 1)
的一半?在x86_64环境下,
extra_rc
占8位,最大值为255,此时再ratain
一次,引用计数为256,就溢出了,需要SideTable
来存储。RC_HALF = 1<<7
,值为128,所以是(extra_rc最大值 + 1)
的一半。为什么要将
(extra_rc最大值 + 1)
的一半分别存储在extra_rc
和SideTable
中?因为每次操作
SideTable
都需要进行一次上锁/解锁,而且还要经过几次哈希运算才能处理对象的引用计数,效率比较低。而且,考虑到release
操作,也不能在溢出时把值全部存在SideTable
中。因此,为了尽可能多的去操作extra_rc
,每当extra_rc
溢出时,就各存一半,这样下次进来就还是直接操作extra_rc
,会更高效。
3.2 release 源码分析
release
和retain
的实现逻辑大体相同,只是将引用计数+1
变为-1
。在源码中release
操作的底层函数调用链为objc_release
-> release
-> rootRelease
,最终实现代码如下:
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
//1.若对象为TaggedPointer对象,直接返回
if (isTaggedPointer()) return false;
//声明一个标记:sideTable是否被锁
bool sideTableLocked = false;
//声明两个isa_t的局部变量,用于新旧值的替换
isa_t oldisa;
isa_t newisa;
retry:
do {
//将对象的isa的赋值给两个局部isa保存
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//2.若对象不是nonpointer类型,直接操作sidetable
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
//3.若对象是nonpointer类型的对象,则将extra_rc值-1
//先通过左移运算获取到isa里的extra_rc,-1后再将新值赋给isa
//carry标记,这里用来表示extra_rc的值是否为0
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
//若extra_rc的值为0,进入underflow
goto underflow;
}
//这个while判断条件里面已经将newisa赋值给对象的isa了
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
//若extra_rc的值大于0,则解锁SideTable,并返回
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
//若extra_rc的值为0,会跳来这里执行
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
//上面这句注释表示这里的处理主要是从SideTable借引用计数或者直接释放对象
// abandon newisa to undo the decrement
newisa = oldisa;
//4.判断对象是否已使用SideTable存储引用计数
//isa的has_sidetable_rc值为1,表示对象已使用SideTable储引用计数
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.
//取出SideTable中存储的当前对象的引用计数值的一半,赋值给borrowed
//这一步操作后,SideTable中存储的值就只剩一半了
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.
//判断borrowed是否大于0
if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
//borrowed大于0,表示SideTable中还存有引用计数,所以不能释放
//borrowed - 1,再把值赋给extra_rc,下次又可以直接操作extra_rc
newisa.extra_rc = borrowed - 1; // redo the original decrement too
//更新isa的值
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
//容错处理,如果extra_rc赋值失败,则再尝试赋值一次
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);
}
}
}
//容错处理,如果extra_rc赋值一直失败,则将之前取出的一半引用计数值还给Sidetable
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并返回
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
//borrowed等于0,表示对象的引用计数也为0,就走后面的dealloc流程
}
}
// Really deallocate.
//5.释放对象
//isa的has_sidetable_rc为0,说明对象没有使用SideTable存储引用计数,而此时extra_rc也为0,即对象的引用计数为0,就直接释放。
if (slowpath(newisa.deallocating)) {
//若当前对象正在释放,则不再执行释放操作,直接解锁SideTable,并返回一个过度释放的错误
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
//将isa的deallocating赋值为1,表示正在执行释放操作
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
//解锁SideTable
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
//发送一个dealloc消息,释放对象
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
经过源码分析可知,release
的实现逻辑如下:
-
Step1
:如果对象为TaggedPointer
对象,则直接返回,不做其他操作。 -
Step2
:如果对象不是nonpointer
类型,若对象为元类对象,则直接返回,若不是元类对象,则直接操作SideTable
,将对象的引用计数-1并保存在RefcountMap
中,当引用计数变为0,则释放对象。 -
Step3
:如果对象是nonpointer
类型,执行extra_rc-1
,判断值是否为0。若不为0,更新isa的值并返回;若为0,则判断是否已经使用SideTable
存储对象的引用计数。 -
Step4
:若isa.has_sidetable_rcw==1
,表示已经使用SideTable
存储对象的引用计数 。则取出SideTable
存储的一半引用计数值,并判断这一半值是否大于0,若大于0,则将(一半值 - 1)
赋值给extra_rc
,若等于0,表示对象的引用计数为0,则释放对象。 -
Step5
:若isa.has_sidetable_rcw==0
,表示没有使用SideTable
存储对象的引用计数。此时对象的引用计数为0,就直接释放对象。若当前正在释放,则不再执行新的释放操作,并返回一个过度释放
的错误。
注意:从
SideTable
中取出一半引用计数值后,SideTable
中存储的值也只剩下一半,如果后续extra_rc
的赋值失败,再将取出的一半值还给SideTable
。在
sidetable_subExtraRC_nolock(RC_HALF)
函数的实现中,有一步it->second = newRefcnt
,就是将计算后的一半值存储在SideTable
中。
3.3 dealloc 源码分析
dealloc
的逻辑就相对简单点,在源码中查看rootDealloc
函数的实现如下:
inline void
objc_object::rootDealloc()
{
//1.若对象为TaggedPointer对象,直接返回
//(吐槽一下,苹果的人员都不确定这步判断是否必要)
if (isTaggedPointer()) return; // fixme necessary?
/**2.若对象为nonpointer类型,并且
*没有被弱引用
*没有关联对象
*没有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
object_dispose((id)this);
}
}
//3.清空对象的相关信息,并释放内存空间
id
object_dispose(id obj)
{
if (!obj) return nil;
//清空对象的相关信息
objc_destructInstance(obj);
//释放内存空间
free(obj);
return nil;
}
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();
// This order is important.
//调用C++析构函数
if (cxx) object_cxxDestruct(obj);
//删除关联对象
if (assoc) _object_remove_assocations(obj);
//释放
obj->clearDeallocating();
}
return obj;
}
inline void
objc_object::clearDeallocating()
{
//若对象不是nonpointer类型,则直接在SideTable中清空对象的相关信息
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//这一步直接清空SideTable中对象的所有信息,包括引用计数和弱引用指针
sidetable_clearDeallocating();
}
//若对象是nonpointer类型,并且在SideTable中存储了弱引用指针或者引用计数
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
//清空弱引用指针和引用计数
clearDeallocating_slow();
}
assert(!sidetable_present());
}
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) {
//将弱引用表中当前对象关联的所有指针都设为nil并移除
weak_clear_no_lock(&table.weak_table, (id)this);
}
//清空引用计数
if (isa.has_sidetable_rc) {
//从引用计数表中移除当前对象的引用计数
table.refcnts.erase(this);
}
//解锁
table.unlock();
}
经过源码分析可知,dealloc
的实现逻辑如下:
-
Step1
:如果对象为TaggedPointer
对象,则直接返回,不做其他操作。 -
Step2
:如果对象是nonpointer
类型,且没有被弱引用,没有关联对象,没有C++
析构器,没有使用SideTable
存储引用计数,则直接释放对象的内存空间。 -
Step3
:如果对象有析构器,则执行C++
析构函数。 -
Step4
:如果对象有关联对象,则删除关联对象。 -
Step5
:如果对象有弱引用指针,则将弱引用表中当前对象关联的所有指针都设为nil
并移除。 -
Step6
:如果对象有在SideTable
中存储引用计数,则将引用计数从表中移除。 -
Step7
:若对象不是nonpointer
类型,则直接在SideTable
中清空对象的相关信息,包括引用计数和弱引用指针。 -
Step8
:执行free
函数,释放对象的内存空间。
四、获取对象的引用计数 -- retainCount()
在iOS
中,获取对象的引用计数有两种方式:
- 方式1:使用
KVC
获取。
[obj valueForKey:@"retainCount"];
- 方式2:使用
CFGetRetainCount
函数获取。
CFGetRetainCount((__bridge CFTypeRef)(obj));
这两个方式在源码工程中通过断点调式可知,都是调用retainCount
函数来获取对象的引用计数,查看函数调用链retainCount
-> _objc_rootRetainCount
-> rootRetainCount
,最终实现如下:
inline uintptr_t
objc_object::rootRetainCount()
{
//1.若对象为TaggedPointer对象,直接返回当前对象
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
//获取isa中的数据
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//2.若对象为nonpointer类型,返回的引用计数为(extra_rc + SideTable_rc + 1)
if (bits.nonpointer) {
//当前引用计数为(extra_rc + 1)
uintptr_t rc = 1 + bits.extra_rc
//若SideTable中存储了对象的引用计数,还需要加上这个引用计数值
if (bits.has_sidetable_rc) {
//注意:这一步加上的是SideTable里存储的真实值,没有+1操作
//详情查看拓展2
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
//返回引用计数
return rc;
}
sidetable_unlock();
//3.若对象不是nonpointer类型,返回(SideTable_rc + 1)
//详情查看拓展3
return sidetable_retainCount();
}
//拓展2:当对象为nonpointer类型时,返回SideTable存储真实的引用计数值
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;,
//返回的是保存的真实值,没有+1操作
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
//拓展3:当对象不是nonpointer类型时,返回(SideTable_rc + 1)
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];
//先将返回值初始化为1,保证最小返回1
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
//将SideTable存储的真实引用计数值+1返回
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
经过源码分析可知,retainCount
的实现逻辑如下:
-
Step1
:如果对象为TaggedPointer
对象,则直接返回当前对象。 -
Step2
:如果对象是nonpointer
类型,返回的引用计数值为rc = (extra_rc + 1 + SideTable_rc)
。 -
Step3
:如果对象不是nonpointer
类型,返回的引用计数值为rc = (SideTable_rc + 1)
。
retainCount
获取到的引用计数比真实值多1,最少为1。
extra_rc
表示isa
中存储的引用计数值,这是系统的标记。
SideTable_rc
表示SideTable
中存储的引用计数值,这是为了书写方便,我自己用的标记。
(extra_rc + 1 + SideTable_rc)
这样将1放在中间的书写顺序,是为了提醒上面拓展2和拓展3这两个函数的区别。
五、关于引用计数的一道面试题
- 问:
alloc
创建的对象,它的引用计数为多少?
//这个NSObject对象的引用计数是多少?
NSObject *obj = [[NSObject alloc] init];
- 答:
alloc
创建的对象引用计数为0。
这道题最简单的解答方式,是直接打印对象的引用计数
NSObject *obj = [[NSObject alloc] init];
NSLog(@" ==== rc = %ld", CFGetRetainCount((__bridge CFTypeRef)(obj)));
//打印结果:
2020-12-28 15:53:31.838325+0800 内存管理[73461:16342967] ==== rc = 1
retainCount
函数获取的引用计数值为1,则真实的引用计数为0,所以alloc
创建的对象,引用计数为0。
5.1 结论分析
为什么引用计数为0?可以将NSObject *obj = [[NSObject alloc] init]
拆解成两步来分析:
-
第一步:通过
alloc
创建一个NSObject
对象。在之前的
alloc核心步骤
相关文章里有分析alloc创建对象的整个流程,最后是在initInstanceIsa
函数里完成了对象的isa
的初始化,实现逻辑如下:- 如果对象不是
nonpointer
类型,将对象的地址赋值给isa
。 - 如果对象是
nonpointer
类型,给isa_t
结构里的nonpointer
、magic
、has_cxx_dtor
、shiftcls
这4个成员进行初始化赋值。
因此,这一步里并没有给
extra_rc
赋值,后续也没有操作extra_rc
,所以alloc
结束后,对象的引用计数为0。initInstanceIsa
的详细介绍可阅读iOS原理 alloc核心步骤3:initInstanceIsa详解 - 如果对象不是
-
第二步:将对象的地址赋值给指针
obj
。给指针赋值的操作,不同于强引用,不会执行
retain
,所以对象的引用计数依旧为0。//给指针obj1赋值,不会retain NSObject *obj1 = [[NSObject alloc] init]; //对象被指针obj2强引用,会retain,引用计数+1 NSObject *obj2 = obj1;
5.1 印证结论
对这个结论可以在源码工程中印证,这里是在objc-781
源码中进行断点调试:
NSObject *obj = [[NSObject alloc] init];
NSLog(@" ==== rc = %ld", CFGetRetainCount((__bridge CFTypeRef)(obj)));
在obj
实例化后打断点,并在lldb
中输出isa
来验证:
(lldb) p obj
(NSObject *) $0 = 0x000000010064b810
//读取obj对象的内存,第一个为成员isa
(lldb) x/4gx $0
0x10064b810: 0x001d800100350141 0x0000000000000000
0x10064b820: 0x64696c53534e5b2d 0x206b636172547265
//打印isa的值
(lldb) p 0x001d800100350141
(long) $1 = 8303516111405377
//这里需要声明成isa_t的结构才能输出
(lldb) p (isa_t)$1
(isa_t) $2 = {
cls = NSObject
bits = 8303516111405377
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 537305128
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
(lldb)
从输出结果来看,alloc创建的对象,extra_rc
的值为0,所以引用计数为0,完美印证。这里也可以直接将isa的值0x001d800100350141
以二进制展开,可以看到extra_rc
的值为0。(图中红色框内为extra_rc
,__x86_64__
环境)
注意,只有在源码工程中才能这样验证,在自己的工程中是不能输出
isa_t
的结构,而且读取内存里的isa
的值,只包含了shiftcls
的信息。
六、总结
感觉上面已经讲的很详细了,这里就只总结几个要点:
SideTable
中包含三个成员:自旋锁(slock)
,引用计数表(RefcountMap)
,弱引用表(weak_table_t)
。引用计数表是个哈希表,用来存储对象的引用计数。弱引用表也是哈希表,用来存放对象的弱引用指针。SideTables
是一个全局的哈希数组,里面存储了多张SideTable
,在iOS
的真机模式下,最多8张,在MacOS
或者模拟器模式下,最多64张。每一个对象对应一个SideTable
,每一个SideTable
存储多个对象的引用计数和弱引用指针。nonpointer
类型的对象,引用计数存储在isa
的extra_rc
和SideTable
的RefcountMap
中。当被retain
或者realease
时,先操作extra_rc
,溢出或者为0时,才操作SideTable
。非nonpointer
类型的对象,引用计数只存储在SideTable
的RefcountMap
中。TaggedPointer
对象,内存由系统管理,不用处理引用计数。当对象被弱引用时,这个弱引用指针会存储在
SideTable
的weak_table_t
中。当对象被释放时,会先执行
C++
析构函数,删除关联对象,清空引用计数,将弱引用指针设为nil
后清空,最后free
释放内存空间。retainCount
获取的引用计数值要比对象的真实值多1,最小为1。alloc
的对象引用计数为0。