文章目錄
- 前言
- 前情知識
- retain和release的實現原理(MRC手動管理)
- retain(MRC手動管理)
- retain源碼
- 內聯函數rootRetain源碼
- 相關的sidetable_tryRetain()方法
- retain底層工作流程總結
- release
- release源碼
- 內聯函數rootRelease源碼
- 小結
前言
??在前面OC的學習中,我們了解到了OC中的關鍵字,今天我們來具體解析一下strong、copy、retain、release的實現原理,他們都是內存管理的核心機制,其底層實現深度依賴引用計數(Retain Count)和運行時(Runtime)的 SideTable機制。
前情知識
??在了解其底層原理之前,我們先來回顧一下引用計數的相關概念。
??OC內存管理的核心是引用計數(ARC 下由編譯器自動管理,MRC 下手動操作)。每個對象有一個隱式的引用計數retainCount),表示當前有多少個“擁有者”持有該對象:
- 引用計數 +1:對象被“持有”(如
retain
、strong
賦值)。 - 引用計數 -1:對象被“釋放”(如
release
、strong
變量超出作用域)。 - 引用計數為 0:對象被銷毀(調用
dealloc
),內存被回收。
??OC對象的內存生命周期由引用計數控制。每個對象的內存頭部(objc_object結構體)都存儲了一個isa指針,以及一個隱藏的引用計數字段(retainCount)。當引用計數變為0時,對象被銷毀(調用dealloc),內存被回收。
引用計數的存儲方式
- 小對象優化:對于小對象(如NSNumber、NSNull等小尺寸對象)
NSNumber:基本數據類型(如 int、float、BOOL等)的對象化包裝
NSNull:空值的對象化表示。NSNull是一個特殊的類,用于表示“空值”(類似其他語言的 null或 None)
- 大對象:對于大對象(如NSObject子類、NSArray等),引用計數存儲在全局的SideTable中。SideTable是一個哈希表,通過對象的地址作為鍵,映射到包含引用計數和弱引用表的條目(side_table_t)。
retain和release的實現原理(MRC手動管理)
??在MRC模式下,retain和release是手動管理對象生命周期的核心方法,直接操作引用計數。
retain(MRC手動管理)
retain源碼
??retain的作用是增加對象的引用計數,確保對象在被持有期間不會被釋放。關于retain這部分的源碼,在objc4_906_main中如下:
inline id
objc_object::retain()
{//確保對象不是小對象,如果是,就直接返回自身,因為小對象的值直接編碼在指針地址中,無需堆分配,不用引用計數ASSERT(!isTaggedPointer());return rootRetain(false, RRVariant::FastOrMsgSend);
}
在retain內聯的rootRetain函數前面,還有一個其的無參數版本:
ALWAYS_INLINE id
objc_object::rootRetain()
{return rootRetain(false, RRVariant::Fast);
}
這段無參數的rootRetain方法版本,核心作用是通過內聯調用簡化代碼,并為特定場景(如內部優化路徑)提供更高效的調用方式,快速觸發對象的引用計數增加功能操作。
內聯函數rootRetain源碼
進入rootRetain后,源碼如下:
ALWAYS_INLINE id
//rootRetain是retain操作的核心實現,負責處理引用計數的原子性增加、多線程安全、內聯引用計數與側表的轉錄等
//參數:tryRetain為false,表示這是一個普通的保留操作,而非嘗試保留(嘗試保留通常用于鎖機制中,避免阻塞)
// RRVariant::FastOrMsgSend是一個枚舉值,指示當前調用路徑是快速路徑或模擬objc_msgSend發送retain消息的場景
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{//檢查對象是否為小對象(如 int、BOOL、小 NSNumber等)。小對象的值直接編碼在指針地址中(無需堆分配),無需引用計數管理if (slowpath(isTaggedPointer())) return (id)this;bool sideTableLocked = false; // 標記側表是否已鎖定(避免多線程競爭)bool transcribeToSideTable = false; // 標記是否需要將內聯計數轉錄到側表isa_t oldisa; // 保存舊的 isa 位域狀態isa_t newisa; //新的 isa 位域狀態(待更新)oldisa = LoadExclusive(&isa().bits); //原子加載當前 isa 位域的原子性(避免其他線程修改導致臟讀)if (variant == RRVariant::FastOrMsgSend) { //表示當前調用是“快速路徑”或“模擬 objc_msgSend發送 retain消息”(如直接調用 [obj retain])// 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())) { //檢查對象的類是否覆蓋了自定義的引用計數邏輯(如 retain/release方法);若有,跳過默認邏輯,直接調用自定義實現ClearExclusive(&isa().bits); // 釋放原子加載的鎖if (oldisa.getDecodedClass(false)->canCallSwiftRR()) { //判斷是否可調用 Swift 的保留函數(swiftRetain),兼容 Swift 對象的引用計數管理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;}}//編譯器宏,指示是否啟用內聯引用計數(現代 iOS/macOS 系統默認啟用):若未啟用內聯引用計數(如舊版本或特殊配置),引用計數完全存儲在側表(SideTable)中,直接調用側表的 tryRetain或 retain方法
#if !ISA_HAS_INLINE_RC// No need for a CAS loop in this case; we aren't changing the ISA pointerClearExclusive(&isa().bits);if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;else return sidetable_retain(sideTableLocked);
#elsedo {transcribeToSideTable = false;newisa = oldisa;if (slowpath(!newisa.nonpointer)) { // 重新檢查是否為非指針 isa(可能被其他線程修改)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 overridesif (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++ 原子增加extra_rcif (slowpath(carry)) { //內聯引用計數溢出(extra_rc超過最大值)// newisa.extra_rc++ overflowedif (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))); //CAS重新提交isa位域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);}
#endifreturn (id)this;
}
從上述代碼中,我們可以知道objc_object::rootRetain
的工作流程以原子性增加對象引用計數為核心,代碼執行的流程大致如下:
首先初始化 sideTableLocked
和 transcribeToSideTable
狀態變量,并通過 LoadExclusive
原子加載當前 isa 位域(oldisa)。隨后進入 do-while
循環,通過 StoreExclusive
原子提交新 isa 位域(newisa),確保多線程下的原子性操作。循環中檢查 newisa 是否為非指針 isa(若為非指針則重置鎖并重試),并根據 tryRetain 標志選擇調用 sidetable_tryRetain
(嘗試保留)或 sidetable_retain
(強制保留)。若對象正在釋放(isDeallocating()
),則清除鎖并根據條件返回 nil或對象自身。若內聯引用計數(extra_rc)溢出(carry 標志為真),則將部分計數轉錄到側表(標記 has_sidetable_rc
)并調整內聯值(保留 RC_HALF)。最終提交 isa 更新后,同步側表數據(若為完整變體)并解鎖,返回對象自身完成引用計數增加。
相關的sidetable_tryRetain()方法
接下來我們再來看涉及到的sidetable_tryRetain()方法的源碼:
bool
objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA //編譯器宏,指示是否支持非指針 isa(如小對象優化的 Tagged Pointer)ASSERT(!isa().nonpointer); //若支持非指針 isa,則當前對象的 isa不能是指針類型(nonpointer標志為 false)。此斷言用于確保非指針 isa場景下,側表操作的正確性(避免對非指針 isa執行側表邏輯)
#endifSideTable& table = SideTables()[this];//SideTables():全局哈希表,存儲每個對象地址對應的 SideTable(通過對象地址哈希映射)。SideTable結構:每個 SideTable包含兩個核心結構:refcnts:引用計數映射表(RefcountMap),記錄對象被弱引用或臨時引用的次數;weak_table_t:弱引用表,存儲指向該對象的弱引用指針(如 __weak變量)。// 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;//原子操作,嘗試在 refcnts中插入當前對象的條目。若條目已存在(it.second為 false),則直接獲取現有條目;若不存在(it.second為 true),則插入新條目,初始值為 SIDE_TABLE_RC_ONE(表示一次弱引用)auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE);auto &refcnt = it.first->second;//根據 try_emplace的結果(it.second)和當前引用計數的狀態(refcnt),分三種情況處理:if (it.second) { //情況一:插入新條目// there was no entry} else if (refcnt & SIDE_TABLE_DEALLOCATING) { //情況二:條目已存在且對象正在被釋放result = false;} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) { //情況三:條目已存在且對象未被釋放refcnt += SIDE_TABLE_RC_ONE;}return result;
}
由此,我們可以看來,objc_object::sidetable_tryRetain()
函數通過側表(SideTable)嘗試為對象添加弱引用,流程如下:
首先斷言確保對象為指針類型(非小對象優化場景),獲取對象對應的側表;通過原子操作 try_emplace
嘗試在側表的引用計數映射(refcnts
)中插入當前對象的條目。若插入成功(新條目),初始化引用計數為 SIDE_TABLE_RC_ONE
(一次弱引用);若條目已存在,檢查對象狀態:若對象正在釋放(deallocating
標志),則嘗試失敗;若未被固定(pinned 標志),則將引用計數加 1。最終返回是否成功嘗試保留對象(true或 false)。
retain底層工作流程總結
至此,我們可以知道retain底層的工作流程圖大致如下:
+----------------------+
| objc_retain() |
+----------------------+|v
+--------------------------+
| 檢查對象是否為 nil? |
+--------------------------+|+-------+-------+| |是(nil) 否| |
返回 nil +-----------------------------+| 調用 obj->retain() |+-----------------------------+|v+----------------------------------------+| objc_object::retain() || || -> 是否啟用 Tagged Pointer? || -> 是否使用 isa-optimized 引用計數? |+----------------------------------------+|+------------+--------------+| |isa優化計數 YES NO+-----------------------------+ +-----------------------------+| 利用 isa 指針中位域計數器 | | 使用 SideTable 哈希表 || 直接在 isa 中的引用計數 +1 | | sidetable_retain() |+-----------------------------+ +-----------------------------+|v+----------------------------------+| 加鎖 -> SideTable.refcnts[obj]+1 || 解鎖 |+----------------------------------+
release
release的作用是減少對象的引用計數,若計數歸零則銷毀對象。release的底層邏輯:
- 檢查對象是否為小對象,若是則直接返回。
- 獲取 SideTable并減少引用計數。
- 若引用計數減至 0:調用 dealloc方法(釋放實例變量、關聯對象等);釋放 SideTable內存(若無其他對象使用)。
release源碼
在objc_906_main中,這部分源碼如下:
inline void
objc_object::release()
{ASSERT(!isTaggedPointer()); // 斷言非小對象rootRelease(true, RRVariant::FastOrMsgSend); // 調用核心釋放函數
}inline void
objc_object::release()
{ASSERT(!isTaggedPointer()); // 斷言非小對象// 快速路徑:無自定義釋放邏輯時,直接操作側表if (fastpath(!ISA()->hasCustomRR())) { // 元類無需釋放(無實例指向)if (!ISA()->isMetaClass())sidetable_release(); // 釋放側表中的引用計數return;}// 自定義釋放邏輯:通過消息發送調用類的 release 方法((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}
內聯函數rootRelease源碼
這部分源碼有很多地方的處理與rootRelease相同,有的不再一一贅述。
ALWAYS_INLINE bool
objc_object::rootRelease()
{return rootRelease(true, RRVariant::Fast);
}ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{if (slowpath(isTaggedPointer())) return false;bool sideTableLocked = false; // 標記側表是否鎖定(避免多線程競爭)isa_t newisa, oldisa; //保存新舊 isa 位域狀態oldisa = LoadExclusive(&isa().bits); // 原子加載當前 isa 位域(多線程安全)if (variant == RRVariant::FastOrMsgSend) { //RRVariant::FastOrMsgSend:表示當前調用是“快速路徑”或“模擬 objc_msgSend發送 release消息”(優化調用流程)// 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())) { //這里跟rootRetain的判斷邏輯一樣,兼容OC和swiftClearExclusive(&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)) { //元類檢查(跟roorRetain一樣)// a Class is a Class forever, so we can perform this check once// outside of the CAS loopif (oldisa.getDecodedClass(false)->isMetaClass()) {ClearExclusive(&isa().bits);return false;}}#if !ISA_HAS_INLINE_RC// Without inline ref counts, we always use sidetablesClearExclusive(&isa().bits);return sidetable_release(sideTableLocked, performDealloc);
#else
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 overridesuintptr_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 decrementnewisa = 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.(從側表借位(最多借 RC_HALF))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 調整內聯計數(借位后減1)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);//重新加載isa并重試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) {this->performDealloc();//執行對象的dealloc方法}return true;//釋放成功
#endif // ISA_HAS_INLINE_RC
}
由此可見, release方法的底層工作流程以==原子性減少對象引用計數為核心。首先通過 isTaggedPointer()
檢查對象是否為小對象(如 int、小 NSNumber等),若是則直接跳過引用計數管理;接著檢查類是否覆蓋自定義 release邏輯(hasCustomRR),若有則調用自定義實現(如 Swift 的 swiftRelease 或通過 objc_msgSend
發送 release 消息);若為元類(isMetaClass)則直接返回(元類無實例指向,無需釋放);對于普通對象,通過 CAS(Compare-And-Swap)原子操作(LoadExclusive/StoreReleaseExclusive)安全減少內聯引用計數(存儲于 isa位域的 extra_rc字段);若內聯計數溢出(extra_rc 減至負數),則從側表(SideTable)借位(最多借 RC_HALF)并調整內聯計數;最終若引用計數歸零且無法借位,標記對象為釋放狀態(isDeallocating
),執行內存屏障確保可見性后調用 dealloc
釋放內存。
小結
??retain/release是MRC模式下引用計數的操作入口,我們可以通過它們直接操作引用計數字段。雖然現在xcode啟用ARC計數,十分方便,但我們還是有必要了解MRC下相關的手動引用計數管理,這有利于我們更好地掌握引用計數機制。