黑箱中的 retain 和 release
https://github.com/Draveness/Analyze/blob/master/contents/objc/黑箱中的%20retain%20和%20release.md
寫在前面
在接口設計時,我們經常要考慮某些意義上的平衡。在內存管理中也是這樣,Objective-C 同時為我們提供了增加引用計數的 retain
和減少引用計數的 release
方法。
這篇文章會在源代碼層面介紹 Objective-C 中 retain
和 release
的實現,它們是如何達到平衡的。
從 retain 開始
如今我們已經進入了全面使用 ARC 的時代,幾年前還經常使用的 retain
和 release
在這裏,我們還要從 retain
方法開始,對內存管理的實現細節一探究竟。
下面是 retain
方法的調用棧:
- [NSObject retain]
└── id objc_object::rootRetain()
└── id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
├── uintptr_t LoadExclusive(uintptr_t *src)
├── uintptr_t addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
├── uintptr_t bits
│ └── uintptr_t has_sidetable_rc
├── bool StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
└── bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
└── uintptr_t addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
調用棧中的前兩個方法的實現直接調用了下一個方法:
- (id)retain {
return ((id)self)->rootRetain();
}
id objc_object::rootRetain() {
return rootRetain(false, false);
}
而 id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
方法是調用棧中最重要的方法,其原理就是將 isa
結構體中的 extra_rc
的值加一。
extra_rc
就是用於保存自動引用計數的標誌位,下面就是 isa
objc-rr-isa-struct
接下來我們會分三種情況對 rootRetain
進行分析。
正常的 rootRetain
這是簡化後的 rootRetain
方法的實現,其中只有處理一般情況的代碼:
id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
isa_t oldisa;
isa_t newisa;
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
} while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));
return (id)this;
}
在這裏我們假設的條件是
isa
中的extra_rc
的位數足以存儲retainCount
。
- 使用
LoadExclusive
加載isa
的值 - 調用
addc(newisa.bits, RC_ONE, 0, &carry)
方法將isa
的值加一 - 調用
StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)
更新isa
的值 - 返回當前對象
有進位版本的 rootRetain
在這裏調用 addc
方法為 extra_rc
加一時,8 位的 extra_rc
可能不足以保存引用計數。
id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
transcribeToSideTable = false;
isa_t oldisa = LoadExclusive(&isa.bits);
isa_t newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
if (carry && !handleOverflow)
return rootRetain_overflow(tryRetain);
}
extra_rc
不足以保存引用計數,並且handleOverflow = false
。
當方法傳入的 handleOverflow = false
時(這也是通常情況),我們會調用 rootRetain_overflow
方法:
id objc_object::rootRetain_overflow(bool tryRetain) {
return rootRetain(tryRetain, true);
}
這個方法其實就是重新執行 rootRetain
方法,並傳入 handleOverflow = true
。
有進位版本的 rootRetain(處理溢出)
當傳入的 handleOverflow = true
時,我們就會在 rootRetain
方法中處理引用計數的溢出。
id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
if (carry) {
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));
sidetable_addExtraRC_nolock(RC_HALF);
return (id)this;
}
當調用這個方法,並且 handleOverflow = true
時,我們就可以確定 carry
一定是存在的了,
因為 extra_rc
已經溢出了,所以要更新它的值為 RC_HALF
:
#define RC_HALF (1ULL<<7)
extra_rc
總共為 8 位,RC_HALF = 0b10000000
。
然後設置 has_sidetable_rc
為真,存儲新的 isa
的值之後,調用 sidetable_addExtraRC_nolock
方法。
bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc) {
SideTable& table = SideTables()[this];
size_t& refcntStorage = table.refcnts[this];
size_t oldRefcnt = refcntStorage;
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
} else {
refcntStorage = newRefcnt;
return false;
}
}
這裏我們將溢出的一位 RC_HALF
添加到 oldRefcnt
中,其中的各種 SIDE_TABLE
宏定義如下:
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1)
#define SIDE_TABLE_RC_ONE (1UL<<2)
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
#define SIDE_TABLE_RC_SHIFT 2
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)
因為 refcnts
中的 64 為的最低兩位是有意義的標誌位,所以在使用 addc
時要將 delta_rc
左移兩位,獲得一個新的引用計數 newRefcnt
。
如果這時出現了溢出,那麽就會撤銷這次的行為。否則,會將新的引用計數存儲到 refcntStorage
指針中。
也就是說,在 iOS 的內存管理中,我們使用了 isa
結構體中的 extra_rc
和 SideTable
來存儲某個對象的自動引用計數。
更重要的是,如果自動引用計數為 1,extra_rc
實際上為 0,因為它保存的是額外的引用計數,我們通過這個行為能夠減少很多不必要的函數調用。
到目前為止,我們已經從頭梳理了 retain
方法的調用棧及其實現。下面要介紹的是在內存管理中,我們是如何使用 release
方法平衡這個方法的。
以 release 結束
與 release 方法相似,我們看一下這個方法簡化後的調用棧:
- [NSObject release]
└── id objc_object::rootRelease()
└── id objc_object::rootRetain(bool performDealloc, bool handleUnderflow)
前面的兩個方法的實現和 retain
中的相差無幾,這裏就直接跳過了。
同樣,在分析 release
方法時,我們也根據上下文的不同,將 release
方法的實現拆分為三部分,說明它到底是如何調用的。
正常的 release
這一個版本的方法調用可以說是最簡版本的方法調用了:
bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
isa_t oldisa;
isa_t newisa;
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
} while (!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits));
return false;
}
- 使用
LoadExclusive
獲取isa
內容 - 將
isa
中的引用計數減一 - 調用
StoreReleaseExclusive
方法保存新的isa
從 SideTable 借位
接下來,我們就要看兩種相對比較復雜的情況了,首先是從 SideTable
借位的版本:
bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
isa_t oldisa;
isa_t newisa;
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
if (carry) goto underflow;
} while (!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits));
...
underflow:
newisa = oldisa;
if (newisa.has_sidetable_rc) {
if (!handleUnderflow) {
return rootRelease_underflow(performDealloc);
}
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
if (borrowed > 0) {
newisa.extra_rc = borrowed - 1;
bool stored = StoreExclusive(&isa.bits, oldisa.bits, newisa.bits);
return false;
}
}
}
這裏省去了使用鎖來防止競爭條件以及調用
StoreExclusive
失敗後恢復現場的代碼。
我們會默認這裏存在SideTable
,也就是has_sidetable_rc = true
。
你可以看到,這裏也有一個 handleUnderflow
,與 retain 中的相同,如果發生了 underflow
,會重新調用該 rootRelease
方法,並傳入 handleUnderflow = true
。
在調用 sidetable_subExtraRC_nolock
成功借位之後,我們會重新設置 newisa
的值 newisa.extra_rc = borrowed - 1
並更新 isa
。
release 中調用 dealloc
如果在 SideTable
中也沒有獲取到借位的話,就說明沒有任何的變量引用了當前對象(即 retainCount = 0
),就需要向它發送 dealloc
消息了。
bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
if (carry) goto underflow;
} while (!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits));
...
underflow:
newisa = oldisa;
if (newisa.deallocating) {
return overrelease_error();
}
newisa.deallocating = true;
StoreExclusive(&isa.bits, oldisa.bits, newisa.bits);
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
}
上述代碼會直接調用 objc_msgSend
向當前對象發送 dealloc
消息。
不過為了確保消息只會發送一次,我們使用 deallocating
標記位。
獲取自動引用計數
在文章的最結尾,筆者想要介紹一下 retainCount
的值是怎麽計算的,我們直接來看 retainCount
方法的實現:
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
inline uintptr_t objc_object::rootRetainCount() {
isa_t bits = LoadExclusive(&isa.bits);
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
return rc;
}
根據方法的實現,retainCount 有三部分組成:
- 1
extra_rc
中存儲的值sidetable_getExtraRC_nolock
返回的值
這也就證明了我們之前得到的結論。
小結
我們在這篇文章中已經介紹了 retain
和 release
這一對用於內存管理的方法是如何實現的,這裏總結一下文章一下比較重要的問題。
extra_rc
只會保存額外的自動引用計數,對象實際的引用計數會在這個基礎上 +1- Objective-C 使用
isa
中的extra_rc
和SideTable
來存儲對象的引用計數 - 在對象的引用計數歸零時,會調用
dealloc
方法回收對象
有關於自動釋放池實現的介紹,可以看自動釋放池的前世今生。
作者:Draveness
鏈接:http://www.jianshu.com/p/fff25c76aba4
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。
黑箱中的 retain 和 release