前言
在iOS中,對象的內存是通過引用計數(Reference Count
)來管理的速客。每當有一個新的強引用指針指向偿洁,對象的引用計數就會+1
,當減少一個強引用指針轿钠,引用計數就會-1
巢钓,當引用計數為0
時,對象就會被銷毀疗垛。
一症汹、引用計數值的存儲
在前面介紹alloc核心步驟的相關文章中有提到,一個nonpointer
類型的對象贷腕,它的引用計數是存放在成員isa_t
里的extra_rc
中背镇。在__arm64__
環(huán)境下咬展,extra_rc
在內存中占19位,在__x86_64__
環(huán)境下瞒斩,占8位破婆。
isa_t
結構里和內存管理相關的成員除extra_rc
外,還有weakly_referenced
济瓢、has_sidetable_rc
以及deallocating
這三個荠割,具體情況可參考iOS原理 alloc核心步驟3:initInstanceIsa詳解。
以__x86_64__
環(huán)境為例旺矾,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
類型對象庞呕,直接在里面存放引用計數。當對象的引用計數變?yōu)?時程帕,會自動將相關的信息從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; // 可能會發(fā)生的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.如果當前對象正在釋放禾进,執(zhí)行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
:如果對象是否正在釋放马澈,則執(zhí)行dealloc流程。 -
Step4
:如果對象是nonpointer
類型弄息,執(zhí)行extra_rc+1
痊班,并判斷extra_rc
是否溢出。若已溢出摹量,則將(extra_rc最大值 + 1)
的一半分別存在isa
的extra_rc
和SideTable
的RefcountMap
中涤伐。
為什么是
(extra_rc最大值 + 1)
的一半?在x86_64環(huán)境下缨称,
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
變?yōu)?code>-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嚎尤,會跳來這里執(zhí)行
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)) {
//若當前對象正在釋放绿鸣,則不再執(zhí)行釋放操作疚沐,直接解鎖SideTable,并返回一個過度釋放的錯誤
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
//將isa的deallocating賦值為1潮模,表示正在執(zhí)行釋放操作
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);
//發(fā)送一個dealloc消息亮蛔,釋放對象
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
經過源碼分析可知,release
的實現邏輯如下:
-
Step1
:如果對象為TaggedPointer
對象再登,則直接返回尔邓,不做其他操作。 -
Step2
:如果對象不是nonpointer
類型锉矢,若對象為元類對象梯嗽,則直接返回,若不是元類對象沽损,則直接操作SideTable
灯节,將對象的引用計數-1并保存在RefcountMap
中,當引用計數變?yōu)?绵估,則釋放對象炎疆。 -
Step3
:如果對象是nonpointer
類型,執(zhí)行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,就直接釋放對象腿倚。若當前正在釋放,則不再執(zhí)行新的釋放操作,并返回一個過度釋放
的錯誤敷燎。
注意:從
SideTable
中取出一半引用計數值后暂筝,SideTable
中存儲的值也只剩下一半,如果后續(xù)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
:如果對象有析構器振坚,則執(zhí)行C++
析構函數薇搁。 -
Step4
:如果對象有關聯對象,則刪除關聯對象渡八。 -
Step5
:如果對象有弱引用指針啃洋,則將弱引用表中當前對象關聯的所有指針都設為nil
并移除。 -
Step6
:如果對象有在SideTable
中存儲引用計數屎鳍,則將引用計數從表中移除宏娄。 -
Step7
:若對象不是nonpointer
類型,則直接在SideTable
中清空對象的相關信息逮壁,包括引用計數和弱引用指針孵坚。 -
Step8
:執(zhí)行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這兩個函數的區(qū)別。
五航罗、關于引用計數的一道面試題
- 問:
alloc
創(chuàng)建的對象禀横,它的引用計數為多少?
//這個NSObject對象的引用計數是多少粥血?
NSObject *obj = [[NSObject alloc] init];
- 答:
alloc
創(chuàng)建的對象引用計數為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
創(chuàng)建的對象七问,引用計數為0。
5.1 結論分析
為什么引用計數為0茫舶?可以將NSObject *obj = [[NSObject alloc] init]
拆解成兩步來分析:
-
第一步:通過
alloc
創(chuàng)建一個NSObject
對象。在之前的
alloc核心步驟
相關文章里有分析alloc創(chuàng)建對象的整個流程刹淌,最后是在initInstanceIsa
函數里完成了對象的isa
的初始化饶氏,實現邏輯如下:- 如果對象不是
nonpointer
類型,將對象的地址賦值給isa
有勾。 - 如果對象是
nonpointer
類型疹启,給isa_t
結構里的nonpointer
、magic
蔼卡、has_cxx_dtor
喊崖、shiftcls
這4個成員進行初始化賦值。
因此雇逞,這一步里并沒有給
extra_rc
賦值荤懂,后續(xù)也沒有操作extra_rc
,所以alloc
結束后塘砸,對象的引用計數為0节仿。initInstanceIsa
的詳細介紹可閱讀iOS原理 alloc核心步驟3:initInstanceIsa詳解 - 如果對象不是
-
第二步:將對象的地址賦值給指針
obj
。給指針賦值的操作掉蔬,不同于強引用廊宪,不會執(zhí)行
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創(chuàng)建的對象,extra_rc
的值為0扭倾,所以引用計數為0淀零,完美印證。這里也可以直接將isa的值0x001d800100350141
以二進制展開膛壹,可以看到extra_rc
的值為0驾中。(圖中紅色框內為extra_rc
唉堪,__x86_64__
環(huán)境)
注意,只有在源碼工程中才能這樣驗證肩民,在自己的工程中是不能輸出
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
中佃延。當對象被釋放時,會先執(zhí)行
C++
析構函數夷磕,刪除關聯對象苇侵,清空引用計數,將弱引用指針設為nil
后清空企锌,最后free
釋放內存空間。retainCount
獲取的引用計數值要比對象的真實值多1于未,最小為1撕攒。alloc
的對象引用計數為0。