retain
release
在曾經的MRC
時代經常活躍在我們眼前,現在的ARC
時代我們很少見到他們了,但是不是說他們完全消失了,而是在編譯階段,編譯器自動給我們插入了,下面我們就去看下他們的實現
首先我們要知道物件的引用計數都是報錯在isa
的extra_rc
與一個全域性的SideTable
的hash表中,然後我們先從retain
方法去分析
找到retain
方法的實現
- (id)retain {
return ((id)self)->rootRetain();
}
id objc_object::rootRetain()
{
return rootRetain(false, false);
}
複製程式碼
這兩個方法只是一個包裝,然後看到有呼叫id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
方法,這個方法是整個retain
的核心
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);//載入isa
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// do not 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++ 將isa的值+1(isa中extra_rc+1)
if (slowpath(carry)) {//extra_rc 不足以儲存引用計數,
// newisa.extra_rc++ overflowed
if (!handleOverflow) {//並且 handleOverflow = false。走該迴圈條件
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);//重新執行rootRetain(bool tryRetain, bool handleOverflow)方法並將handleOverflow設定為true
}
// 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;//因為 extra_rc 已經溢位了,所以要更新它的值為 RC_HALF:二進位制 10000000
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));//StoreExclusive(&isa.bits, oldisa.bits, newisa.bits) 更新 isa 的值
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;
}
複製程式碼
這個方法會分為兩種情況,一種是在引用計數沒有超出extra_rc
的位數,在前面我們分析過extra_rc
在arm64架構下為19位,一種是引用計數已經超出了extra_rc
位數,如果沒有超出那麼情況很簡單,只是單純的進行extra_rc+1
操作,其核心就是
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
複製程式碼
這句程式碼就是將extra_rc
進行加一,但是緊跟著我們可以看到又有一個if判斷,這裡面就是處理,如果引用計數超出所做的處理
if (slowpath(carry)) {//extra_rc 不足以儲存引用計數,
// newisa.extra_rc++ overflowed
if (!handleOverflow) {//1, handleOverflow = false。走該迴圈條件
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);//重新執行rootRetain(bool tryRetain, bool handleOverflow)方法並將handleOverflow設定為true
}
// 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;
}
複製程式碼
首先判斷了引用計數是否超出,然後在進行一次判斷如果handleOverflow == false
的時候直接return rootRetain_overflow(tryRetain);
,我們上面可以看到,在呼叫rootRetain
方法的時候, handleOverflow
傳入的就是false
所以這個方法一定會走
id
objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}
複製程式碼
rootRetain_overflow
的方法很簡單,就是將handleOverflow
設定為true
然後重新呼叫rootRetain
方法,之後將extra_rc
值設定為RC_HALF
這個巨集,並將has_sidetable_rc
與transcribeToSideTable
設定為true
# if __arm64__
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define RC_HALF (1ULL<<7)
複製程式碼
跳出迴圈後因為transcribeToSideTable
為true
將呼叫bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
方法
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//將引用計數的一半保留到表中
sidetable_addExtraRC_nolock(RC_HALF);
}
複製程式碼
我們看下sidetable_addExtraRC_nolock
的實現
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)//yty delta_rc = RC_HALF 1ULL<<18
{
assert(isa.nonpointer);
SideTable& table = SideTables()[this];//以物件地址為key獲取對應的SideTable
size_t& refcntStorage = table.refcnts[this];
size_t oldRefcnt = refcntStorage;
// isa-side bits should not be set here
assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
/*
delta_rc << SIDE_TABLE_RC_SHIFT
SIDE_TABLE_RC_SHIFT == 2 1ULL<<20 因為 refcnts 中的 64 為的最低兩位是有意義的標誌位,所以在使用 addc 時要將 delta_rc 左移兩位,獲得一個新的引用計數 newRefcnt。
64位的倒數第一位標記當前物件是否被weak指標指向(1:有weak指標指向); 64位的倒數第二位標記當前物件是否正在銷燬狀態(1:處在正在銷燬狀態) 其他的62位都可以用於儲存retainCount.
*/
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {//儲存多餘的retaincount
refcntStorage = newRefcnt;
return false;
}
}
複製程式碼
在這個方法中我們可以看到首先以物件本身的地址為key取出對應的SideTable
SideTable是一個全域性的hash表,它裡面儲存了多出的引用計數與
weak
指標
然後從SideTable
中的RefcountMap refcnts
再以地址為key取出當前的引用計數refcntStorage
,然後在原始的引用計數的基礎上加上傳入的引用計數<<2
向右偏移兩位的原因是,
RefcountMap refcnts
的最後兩位有特殊的標示意義
倒數第一位標記當前物件是否被weak指標指向(1:有weak指標指向);
倒數第二位標記當前物件是否正在銷燬狀態(1:處在正在銷燬狀態);
所以,64位環境下只有62位是儲存溢位的引用計數的
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1)
複製程式碼
再往下我們可以看到,SideTable
中的引用計數也是有可能會溢位的,這時候,就撤銷這次的行為;否則將新的引用計數儲存進去
由此我們可以得到,當物件的引用計數沒有超出extra_rc
時儲存在extra_rc
,而超出後則溢位的部分儲存在SideTable
中
上面我們瞭解了retain
是怎麼對引用計數進行+
操作的,下面我們去看看release
對引用計數怎麼進行-
操作的,首先
- (oneway void)release {
((id)self)->rootRelease();
}
bool
objc_object::rootRelease()
{
return rootRelease(true, false);
}
複製程式碼
與retain
類似rootRelease
方法是整個release
的核心
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);//獲取isa
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc-- 將 isa 中的引用計數減一
if (slowpath(carry)) {//如果是從SideTable借位了
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));//呼叫 StoreReleaseExclusive 方法儲存新的 isa
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {//判斷是否藉助Sidetable儲存引用計數
if (!handleUnderflow) {//與retain作用相似 重新呼叫本方法(遞迴) rootRelease(bool performDealloc, bool handleUnderflow)並將handleUnderflow=true
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
goto retry;
}
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);//試圖從side table中刪除計數 並返回所刪除的引用計數
if (borrowed > 0) {//借出的引用計數大於0
// 嘗試將引用計數放入extra_rc中
newisa.extra_rc = borrowed - 1;
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {//放入extra_rc失敗
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) {//再試一次依舊不能將多餘的引用計數放入isa中,於是重新將多餘的引用計數在放入side table中
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}
sidetable_unlock();
return false;
}
else {
//Side table是空的不需要做處理了 去做dealloc操作
}
}
if (slowpath(newisa.deallocating)) {
//當前物件正在釋放
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
}
newisa.deallocating = true;//將deallocating設定為true 標誌正在釋放中
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__sync_synchronize();
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);//如果可以釋放 直接呼叫objc_msgSend呼叫dealloc方法
}
return true;
}
複製程式碼
相對於retain
操作,release
就相對會複雜一些,首先判斷是否是isTaggedPointer
,如果是則return
Tagged Pointer
是對NSNumber NSDate
等的一些的優化
然後還是分兩大種情況一種是沒有發生借位操作,只是將引用計數單純的進行-1
操作
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
複製程式碼
如果發生了借位,則又會分為兩種情況,首先判斷newisa.has_sidetable_rc
是否為1,若為1則執行
if (!handleUnderflow) {//與retain作用相似 重新呼叫本方法(遞迴) rootRelease(bool performDealloc, bool handleUnderflow)並將handleUnderflow=true
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
複製程式碼
類似於retain
時的操作
然後從SideTable
中借出RC_HALF
這麼多位,然後將這個值-1後賦值給extra_rc
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);//試圖從side table中刪除計數 並返回所刪除的引用計數
if (borrowed > 0) {//借出的引用計數大於0
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
}
複製程式碼
下面緊跟著防止更新失敗後再一次賦值操作,如果再次失敗,則將資料重新放入表中;
第二種情況,如果Side table
為空,則至今進行dealloc
,首先將isa
的deallocating
設定為true
,然後直接呼叫dealloc
方法。自此我們分析完了retain
與release
的實現,那dealloc
的時候又做了什麼呢?
老套路,先看下delloc
的方法呼叫
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
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((id)this);
}
}
複製程式碼
在rootDealloc
中可以看出來,如果這個isa
是優化過的並且不包含/不曾經包含weak
指標且沒有關聯物件且沒有c++
的析構方法且引用計數沒有超出上限的時候可以快速釋放,否則呼叫object_dispose
方法
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.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製程式碼
可以看到會呼叫移除關聯物件的方法並且呼叫解構函式,然後呼叫了clearDeallocating
方法
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
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());
}
複製程式碼
這個方法判斷了是否是優化過後的isa
,然後呼叫sidetable_clearDeallocating
或clearDeallocating_slow
,這兩個方法都是對SideTable
這個hash表進行一個清理,刪除引用計數與weak表
那麼我們自己在類中所寫的屬性是什麼時候被釋放的呢?我們去看下,首先我們先去看看object_cxxDestruct
方法
void object_cxxDestruct(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
複製程式碼
呼叫了object_cxxDestructFromClass
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);
// Call cls's dtor first, then superclasses's dtors.
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}
複製程式碼
這個方法中會從自己本身的類開始尋找.cxx_destruct
方法,如果找到就呼叫,然後一直往上級的父類找迴圈呼叫,這個方法是C++的析構方法,我們的物件都是在這被釋放的,那麼這個.cxx_destruct
方法是怎麼出現在我們的類裡面的?
寫兩個類
@interface Person : NSObject
@property (nonatomic, copy) NSString * name;
@end
@implementation Person
@end
@interface Student : Person
@property (nonatomic, strong) NSObject * objc;
@end
@implementation Student
- (void)setObjc:(NSObject *)objc{}
- (NSObject *)objc{ return nil;}
@end
複製程式碼
可以看出當類擁有自己的例項變數(非property)時,編譯器會自動的給我們新增如果宣告一個屬性後自己手動將setter,getter方法寫出來後,編譯器不會將我們生成例項變數 然後測試
.cxx_destruct
方法
最後在簡單的說下weak
吧,剛剛在看SideTable
的時候可以看到,在SideTable
中有RefcountMap refcnts;
與weak_table_t weak_table;
兩個,RefcountMap
我們都知道是儲存引用計數的,而weak_table_t
正是儲存當前物件有被哪些weak
指標引用了
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
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];
};
};
}
複製程式碼
referent
==>物件地址,用於weak_entry_t
陣列遍歷時的比對;
聯合體內struct1
->weak_referrer_t *referrers;
聯合體內struct2
->inline_referrers[WEAK_INLINE_COUNT]
weak
變數的指標個數不超過4個用inline_referrers
,
weak
變數的指標個數超過4個用referrers
.
小計:
1,物件的引用計數都是儲存在extra_rc
與SideTable
中;
2,物件擁有成員變數時編譯器會自動插入.cxx_desctruct
方法用於自動釋放;
3,SideTable
中儲存了溢位的引用計數與weak
指標
存疑:
在自身除錯的時候發現如果子類的dealloc
方法被呼叫後也會呼叫父類的dealloc
,這是常識大家都知道,但是不理解為什麼需要呼叫,dealloc中就解除了自己本身的關聯物件,weak
指標然後釋放了所有成員變數,而且在釋放成員變數的時候會向上找自己的父類,那麼這時候呼叫[super dealloc]
的話有什麼意義嗎?
已解決:呼叫[super dealloc]
的原因是有可能父類中有在dealloc
中處理一些別的事情,並且dealloc
的操作是在頂級父類NSObject
中實現的,如果不呼叫super
就不會執行釋放操作了(很簡單的一個事情被我自己給搞的那麼糾結...)
文章參考:
黑箱中的 retain 和 release
iOS Objective-C底層 part3:live^reference
iOS Objective-C底層 part4:die