物件的引用計數與dealloc

不會騎名字發表於2018-06-20

retain release在曾經的MRC時代經常活躍在我們眼前,現在的ARC時代我們很少見到他們了,但是不是說他們完全消失了,而是在編譯階段,編譯器自動給我們插入了,下面我們就去看下他們的實現

首先我們要知道物件的引用計數都是報錯在isaextra_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_rctranscribeToSideTable設定為true

# if __arm64__
#       define RC_HALF  (1ULL<<18)
# elif __x86_64__
#       define RC_HALF  (1ULL<<7)
複製程式碼

跳出迴圈後因為transcribeToSideTabletrue將呼叫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,首先將isadeallocating設定為true,然後直接呼叫dealloc方法。自此我們分析完了retainrelease的實現,那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_clearDeallocatingclearDeallocating_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
複製程式碼

如果宣告一個屬性後自己手動將setter,getter方法寫出來後,編譯器不會將我們生成例項變數 然後測試

物件的引用計數與dealloc
可以看出當類擁有自己的例項變數(非property)時,編譯器會自動的給我們新增.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_rcSideTable中;
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

相關文章