OC源碼 —— retain和release

retain/release兩個(gè)關(guān)鍵字現(xiàn)在已經(jīng)很少見了,但了解一下底層的實(shí)現(xiàn)還是能幫助我們更深刻的理解oc的內(nèi)存管理。


retain

通常情況下,當(dāng)我們對(duì)一個(gè)對(duì)象調(diào)用retain方法時(shí),調(diào)用的順序是這樣的:

[NSObject retain];

- (id)retain {
    return ((id)self)->rootRetain();
}

id objc_object::rootRetain()
{
    return rootRetain(false, false);
}

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);
        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();
        }
        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); 

        if (slowpath(carry)) {
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            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)));

    if (slowpath(transcribeToSideTable)) {
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

最后這個(gè)方法:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow)

看起來(lái)有點(diǎn)復(fù)雜,但沒關(guān)系,我會(huì)分成幾種情況來(lái)分析。

在分析之前需要先回顧一下在Runtime源碼 —— 對(duì)象、類和isa中介紹過isa的結(jié)構(gòu),其中有這么兩個(gè)字段:

// 對(duì)象的引用計(jì)數(shù)太大,無(wú)法存儲(chǔ)
uintptr_t has_sidetable_rc : 1;

// 對(duì)象的引用計(jì)數(shù)超過1,比如10,則此值為9
uintptr_t extra_rc : 8;

這兩個(gè)字段在上面那個(gè)方法中也出現(xiàn)了,從注釋來(lái)看,一個(gè)字段用來(lái)存儲(chǔ)引用計(jì)數(shù)的數(shù)值,另一個(gè)標(biāo)記引用計(jì)數(shù)是否溢出。下面就分溢出與否兩種情況來(lái)討論。

不溢出

最簡(jiǎn)單的情況當(dāng)然是不溢出,這種情況下,rootRetain()方法可以簡(jiǎn)寫如下:

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 (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    return (id)this;
}

這里面的幾個(gè)方法都需要解釋一下,按照順序來(lái):

這里的實(shí)現(xiàn)版本是x86_64

LoadExclusive
LoadExclusive(&isa.bits)

static uintptr_t LoadExclusive(uintptr_t *src)
{
    return *src;
}

第一個(gè)很簡(jiǎn)單,就是獲取isa的內(nèi)容。

addc
addc(newisa.bits, RC_ONE, 0, &carry)
#       define RC_ONE   (1ULL<<56)

static uintptr_t addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
{
    return __builtin_addcl(lhs, rhs, carryin, carryout);
}

我搜不到__builtin_addcl方法的定義或者說(shuō)明文檔,我只能根據(jù)測(cè)試結(jié)果來(lái)做一些猜測(cè)。測(cè)試的過程是這樣的:

addc非溢出測(cè)試.png

首先我在rootRetain()方法中添加了兩個(gè)斷點(diǎn),一個(gè)在while內(nèi)部,一個(gè)在外部,運(yùn)行程序進(jìn)入第一個(gè)斷點(diǎn),獲取了一下這個(gè)時(shí)候isa的內(nèi)容:

(lldb) p &isa
(isa_t *) $0 = 0x0000600000064f40
(lldb) p *$0
(isa_t) $1 = {
  cls = NSThread
  bits = 8444248074519609
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 17592032694407
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

方法結(jié)束之后,看到carry的值是0。接著運(yùn)行進(jìn)入第二個(gè)斷點(diǎn),再獲取一下isa的內(nèi)容:

(lldb) p *$0
(isa_t) $2 = {
  cls = NSThread
  bits = 80501842112447545
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 17592032694407
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 1
  }
}

看到變化了沒有,extra_rc的值增加了1。這個(gè)時(shí)候可以做一些猜測(cè),首先根據(jù)isa_t結(jié)構(gòu)體,extra_rc是在第57~64位,RC_ONE就是第57位為1,addc()方法結(jié)束之后,extra_rc從0->1,相當(dāng)于兩者相加了,沒有溢出,返回的carry值為0。

再測(cè)試一下溢出的情況,需要調(diào)整一下斷點(diǎn)的位置如圖:

addc溢出測(cè)試.png

進(jìn)入第一個(gè)斷點(diǎn)獲取一下extra_rc的值,因?yàn)楂@取的時(shí)isa的內(nèi)容,所以還是oldisa的值:

(lldb) p &isa
(isa_t *) $3 = 0x0000000100882a00
(lldb) p *$3
(isa_t) $4 = {
  cls = MTLIGAccelDevice
  bits = 18382989995916412357
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 1
    shiftcls = 553978040
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 255
  }
}

值為255,加1就會(huì)溢出,所以carry的值為1,繼續(xù)運(yùn)行進(jìn)入第二個(gè)斷點(diǎn),再獲取一下isa:

(lldb) p *$3
(isa_t) $5 = {
  cls = MTLIGAccelDevice
  bits = 9267704350118528453
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 1
    shiftcls = 553978040
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 1
    extra_rc = 128
  }
}

extra_rc從255->128,has_sidetable_rc從0->1,這里邊的過程一會(huì)兒講溢出的時(shí)候再說(shuō)。

從這個(gè)結(jié)果又可以做一點(diǎn)猜測(cè),如果addc的前兩個(gè)參數(shù)加起來(lái)溢出了,carry的值就會(huì)變化,反正不等于0了,具體怎么變化,是不是存儲(chǔ)溢出的值就不得而知了。這個(gè)方法就先這樣了,大概的意思是懂了。

StoreExclusive
StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)

static bool StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
{
    return __sync_bool_compare_and_swap((void **)dst, (void *)oldvalue, (void *)value);
}

方法內(nèi)部又調(diào)用了一個(gè)方法,在gcc的文檔中這個(gè)方法是這樣解釋的:

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
...
These builtins perform an atomic compare and swap. That is, if the current value of *ptr is oldval, then write newval into *ptr.
The “bool” version returns true if the comparison is successful and newval was written. 

應(yīng)用到方法中去,就是如果&isa.bits和oldisa.bits相等,那么就把newisa.bits的值賦給&isa.bits,并且返回true。

在這里&isa.bits和oldisa.bits當(dāng)然是相等的,所以while判斷一次就結(jié)束了。

這3個(gè)方法講清楚之后再回去看看簡(jiǎn)化版的rootRetain()方法就很簡(jiǎn)單了,其實(shí)就是給extra_rc+1,然后更新一下isa的內(nèi)容。

有溢出

有溢出的時(shí)候,情況稍微復(fù)雜一點(diǎn),rootRetain()方法在這個(gè)時(shí)候會(huì)變成這樣:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    uintptr_t carry;
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); 
    
    if (!handleOverflow) {
        ClearExclusive(&isa.bits);
        return rootRetain_overflow(tryRetain);
    }
}

id objc_object::rootRetain_overflow(bool tryRetain)
{
    return rootRetain(tryRetain, true);
}

方法的前半部分一模一樣,但是因?yàn)橐绯隽?,所以后面的路線變化了,直接調(diào)用了rootRetain_overflow()方法,這個(gè)方法內(nèi)部又調(diào)用了rootRetain()方法。

不同的是,首次調(diào)用兩個(gè)參數(shù)都是false,而這次調(diào)用,第二個(gè)參數(shù)為true。所以這個(gè)時(shí)候的rootRetain()方法又可以重新簡(jiǎn)化一下:

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);

        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    sidetable_addExtraRC_nolock(RC_HALF);

    return (id)this;
}
#       define RC_HALF  (1ULL<<7)

奇怪的是,看看完整版的rootRetain()方法,第二個(gè)參數(shù)并沒有其他用處,為什么需要繞個(gè)彎再調(diào)用一下自己,而不是直接繼續(xù)執(zhí)行后面的代碼呢?

源代碼中對(duì)此的注釋只有:

// handleOverflow=false is the frameless fast path.
// handleOverflow=true is the framed slow path including overflow to side table
// The code is structured this way to prevent duplication.

但是就從源代碼來(lái)看,直接將這個(gè)參數(shù)去掉,rootRetain()方法只留下第一個(gè)參數(shù)都是可以的。甚至還減少了函數(shù)調(diào)用的次數(shù)。

先不管這個(gè)疑問,這一次,更新了newisa的extra_rc和has_sidetable_rc字段,將extra_rc設(shè)置為了RC_HALF也就是128,has_sidetable_rc設(shè)為true,這就是為什么上面測(cè)試溢出時(shí)輸出結(jié)果為:

extra_rc = 128
has_sidetable_rc = 1

就是在此處設(shè)置的。

這里就有一個(gè)疑問了,講道理這個(gè)時(shí)候extra_rc應(yīng)該為256,這里值為128,還有128哪里去了?

這就是最后那個(gè)方法做的事情了:

sidetable_addExtraRC_nolock(RC_HALF);

bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    SideTable& table = SideTables()[this];

    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);
    if (carry) {
        refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  
#define SIDE_TABLE_RC_ONE            (1UL<<2)
#define SIDE_TABLE_RC_SHIFT 2

SideTable看起來(lái)就是用來(lái)存儲(chǔ)引用計(jì)數(shù)的??唇Y(jié)構(gòu)體SideTable的源碼,應(yīng)該和weak reference也有點(diǎn)關(guān)系,以后研究weak的時(shí)候再深入吧,目前我也不是很清楚。

現(xiàn)在只需要注意到在獲取到oldRefcnt之后,緊跟著兩個(gè)assert,從宏的定義來(lái)看,分別是第一和第二位,assert是為了確保oldRefcnt低兩位為0,因?yàn)榈蛢晌皇怯凶饔玫臉?biāo)志位。也就是說(shuō)引用計(jì)數(shù)實(shí)際上是從第三位開始存放的。

再向后看,這就解釋了addc()第二個(gè)參數(shù)左移了兩位進(jìn)行累加了。

這里再一次進(jìn)行了溢出判斷,如果溢出了,存放的引用計(jì)數(shù)被置為了SIDE_TABLE_RC_PINNED,因?yàn)?oldRefcnt & SIDE_TABLE_FLAG_MASK)結(jié)果一定是0,但是我不理解這里為什么要這么做。

同樣不能理解的還有addc之前的if判斷。是不是引用計(jì)數(shù)太大,就不處理了呢?畢竟有62位用來(lái)存,這要是再溢出了就無(wú)法想象了。

驗(yàn)證一下正常的結(jié)果:

addExtraRC不溢出.png

傳入的參數(shù)是RC_HALF,也就是128,在addc之后,newRefcnt變?yōu)?12 = 128 << 2也沒問題,carry為0沒有溢出,最后更新了一下存放的引用計(jì)數(shù)值。

這里稍微匯總一下,正常情況下,retain方法就是給extra_rc加1,當(dāng)extra_rc溢出時(shí),將一半的引用計(jì)數(shù)存放到SideTable中。

一個(gè)合理的疑問是,當(dāng)extra_rc再次溢出的時(shí)候呢?很容易測(cè)試,只需要再進(jìn)一次斷點(diǎn),如下圖:

同一個(gè)對(duì)象extra_rc第二次溢出.png

這個(gè)時(shí)候oldRefcnt已經(jīng)是512 = 128 << 2,也就是上一步存進(jìn)去的結(jié)果,addc之后又加了128進(jìn)去。說(shuō)白了每次extra_rc溢出了,SideTable中就增加128。

到這里,關(guān)于retain方法的源碼已經(jīng)看的差不多了,但是還有疑問:

為什么每次只移動(dòng)一半的引用計(jì)數(shù)(也就是128)到SideTable中?

我也不知道。


release

在深入理解了retain之后,再看release的源碼就很簡(jiǎn)單了。代碼太長(zhǎng)我就不貼了,總結(jié)一下release的流程:

  1. 最普通的情況,直接將extra_rc減1
  2. 如果extra_rc為0,判斷has_sidetable_rc
  3. has_sidetable_rc = false,說(shuō)明對(duì)象已經(jīng)沒有引用計(jì)數(shù)了,直接dealloc釋放內(nèi)存
  4. has_sidetable_rc = true,說(shuō)明extra_rc有過溢出
  5. 從SideTable中借位成功,每次取RC_HALF,也就是128,減1之后賦給extra_rc,回到步驟1
  6. 從SideTable中借位失敗,直接dealloc

retainCount

最后再說(shuō)一下和retain有關(guān)的一個(gè)方法,就是獲取引用計(jì)數(shù)值retainCount,這個(gè)方法其實(shí)不看源碼也能想象出來(lái)是怎么計(jì)算的:

retainCount = 1+ extra_rc + SideTable中存儲(chǔ)的rc

源碼貼一下作為結(jié)尾:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

inline uintptr_t 
objc_object::rootRetainCount()
{
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    return sidetable_retainCount();
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容