Objective-C 小記(7)retain & release

本文使用的 runtime 版本為 objc4-706筑辨。

retain

retain 在現(xiàn)在的 runtime 中的默認實現(xiàn)是 objc_object 中的 retain 函數(shù)获黔,可以在 objc-object.h 中找到它:

// Equivalent to calling [this retain], with shortcuts if there is no override
inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

retain 函數(shù)首先斷言對象指針不是一個 tagged pointer(assert(!isTaggedPointer()))簿训,之后對 isa 中是否有自定義 retainrelease 實現(xiàn)標示位進行判斷骇径,如果沒有自定義的實現(xiàn),則進入默認實現(xiàn) rootRetain 函數(shù)缭贡,否則的話直接向?qū)ο蟀l(fā)送 retain 消息炉擅,調(diào)用自定義的 retain 實現(xiàn)辉懒。

本文的關(guān)注點當(dāng)然是在默認實現(xiàn)上,所以繼續(xù)查看 rootRetain 函數(shù)的實現(xiàn):

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

rootRetain 函數(shù)的實現(xiàn)是調(diào)用了另一個重載的 rootRetain谍失。

在繼續(xù)對下面的代碼進行分析之前眶俩,先回顧一下 isa 的結(jié)構(gòu)(這里只對 x86-64 架構(gòu)的 isa_t 進行分析):

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };
};

《Objective-C 小記(2)對象 2.0》中有對 isa_t 更詳細的描述。現(xiàn)在需要關(guān)心的是 has_sidetable_rcextra_rc 這兩個位字段(bit-field)快鱼。extra_rc 表示「額外的 retain count」颠印,假如 extra_rc 的值為 2,則對象的引用計數(shù)為 3抹竹∠吆保回顧《Objective-C 小記(6)alloc & init》可以發(fā)現(xiàn),對象在創(chuàng)建時 extra_rc 的值是 0窃判,引用計數(shù)則是 1钞楼。還可以注意到 extra_rc 只有 8 位,這樣它最多能記到 255袄琳,如果這個時候引用計數(shù)還要往上增加怎么辦呢询件?這時候?qū)ο髸⒁话氲囊糜嫈?shù)存儲到一個表里,并將 has_sidetable_rc 置為 1跨蟹。

回到 rootRetain 函數(shù):

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

函數(shù)一開始又檢查了自己是不是 tagged pointer(if (isTaggedPointer()) return (id)this;)雳殊,這難道就是防御式編程?

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

它首先聲明了四個變量窗轩,四個變量都能從名字知道它們的用處:

  • sideTableLocked,用來表示 side table 是否鎖上了
  • transcribeToSideTable座咆,用來表示是否需要將 isa 中的引用計數(shù)轉(zhuǎn)移到 side table 里去
  • oldisa痢艺,isa 本來的值
  • newisaisa 新的值(增加了引用計數(shù)后的值)
    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();
        }
        // don't 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;
        }

之后進入 do-while 循環(huán)介陶,循環(huán)里首先將 transcribeToSideTable 賦值為 false堤舒,oldisanewisa 賦值為 isa.bits 的值(LoadExclusive 的作用是讓讀取操作原子化,根據(jù) CPU 不同實現(xiàn)不同哺呜,比如在 x86-64 上就是單純的直接返回值舌缤,而在 arm64 上則使用了 ldxr 指令)。

關(guān)于 slowpathfastpath 宏某残,在《Objective-C 小記(6)alloc & init》中有解釋国撵。

關(guān)于 tryRetain,這個參數(shù)與 weak 的實現(xiàn)有關(guān)玻墅,本文暫不做分析介牙。

首先會檢查 isa 是不是 non-pointer(if (slowpath(!newisa.nonpointer)) { ... }),如果不是 non-pointer澳厢,就進入 sidetable_retain 這個過程环础,這是完全由一個表來存放引用計數(shù)的實現(xiàn)囚似。

第二個判斷則是和 tryRetain 有關(guān),暫時不做分析线得∪幕剑可以發(fā)現(xiàn)這兩個判斷使用的都是 slowpath,表示是不太可能出現(xiàn)的情況贯钩。

        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                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)));

接下來就是重點部分了募狂,聲明 carry 變量來標示是否溢出。然后使用 addc(newisa.bits, RC_ONE, 0, &carry)newisaextra_rc 位字段加 1魏保。這里有個判斷是否溢出熬尺,如果溢出的話還要判斷 handleOverflow 是否為 true,可以注意到這個函數(shù)被調(diào)用時 hadleOverflowfalse谓罗,需要進入 rootRetain_overflow 函數(shù)粱哼,而 rootRetain_overflow 的實現(xiàn)是這樣的:

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

它又重新調(diào)用 rootRetain,不過將 handleOverflow 置為了 true檩咱,希望有大神分享一下為什么要這樣做……rootRetain 里剩余的工作也很好理解揭措,將 side table 鎖住,給 sideTableLockedtranscribeToSideTable 設(shè)置好值刻蚯,extra_rc 留下一半(在 x86-64 下就是 126)的引用計數(shù)绊含,并將 has_sidetable_rc 設(shè)置為 true

最后 while 里的操作是對比 isaoldisa 的值炊汹,如果一樣則將 newisa 覆蓋 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;
}

最后讨便,函數(shù)檢查 transcribeToSideTable充甚,也就是如果之前的操作有溢出,則將一半的引用計數(shù)加到表里霸褒。

release

release 的實現(xiàn)也在 objc-object.h 中:

// Equivalent to calling [this release], with shortcuts if there is no override
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}

retain 基本上是一致的伴找,如果有自定義實現(xiàn)的話,則發(fā)消息废菱,否則進入默認實現(xiàn) rootRelease

ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

套路真是一模一樣技矮,繼續(xù)看 rootRelease 的實現(xiàn):

ALWAYS_INLINE 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);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }

同樣也是進入一個 do-while 循環(huán)衰倦,套路滿滿,這里也不解釋了梳凛。

        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_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(sideTableLocked)) sidetable_unlock();
    return false;

使用 subc(newisa.bits, RC_ONE, 0, &carry)newisa 的引用計數(shù)減 1耿币,發(fā)現(xiàn)下溢出后跳轉(zhuǎn)到 underflow。如果沒有溢出韧拒,函數(shù)就這樣結(jié)束了淹接。繼續(xù)看 underflow 的代碼:

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

首先將 newisa 重制十性,然后判斷這個對象有沒有 side table,有的話塑悼,可以把 side table 里的引用計數(shù)移過來劲适。但判斷里面又是判斷 handleUnderflow 這個參數(shù),rootRelease_underflow 的實現(xiàn)也是和 rootRetain_overflow 差不多的:

NEVER_INLINE bool 
objc_object::rootRelease_underflow(bool performDealloc)
{
    return rootRelease(performDealloc, true);
}

總之調(diào)用了這個函數(shù)還是會回到上面的代碼厢蒜,就繼續(xù)往下看吧:

        // 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.
            goto retry;
        }

首先將 side table 鎖住霞势,為了防止出現(xiàn)競爭又跑一遍 retry

        // Try to remove some retain counts from the side table.        
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // 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.
                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) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

這里從 side table 借 RC_HALF 的引用計數(shù)放到 extra_rc 上斑鸦。接下來的代碼是從 side table 借不到的情況愕贡,那當(dāng)然就是對象需要被銷毀了。

    // Really deallocate.

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.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);
    }
    return true;
}

可以看到巷屿,就是直接就發(fā)送了 dealloc 消息固以。

總結(jié)

對于現(xiàn)在的 non-pointer isa 來說,引用計數(shù)一部分存儲在 isa 的 extra_rc 上嘱巾,溢出后轉(zhuǎn)移到一個表里憨琳。感覺是個很有意思的實現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旬昭,一起剝皮案震驚了整個濱河市篙螟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌问拘,老刑警劉巖遍略,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異骤坐,居然都是意外死亡墅冷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門或油,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驰唬,你說我怎么就攤上這事顶岸。” “怎么了叫编?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵辖佣,是天一觀的道長。 經(jīng)常有香客問我搓逾,道長卷谈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任霞篡,我火速辦了婚禮世蔗,結(jié)果婚禮上端逼,老公的妹妹穿的比我還像新娘。我一直安慰自己污淋,他們只是感情好顶滩,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寸爆,像睡著了一般礁鲁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赁豆,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天仅醇,我揣著相機與錄音,去河邊找鬼魔种。 笑死析二,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的务嫡。 我是一名探鬼主播甲抖,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼心铃!你這毒婦竟也來了准谚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤去扣,失蹤者是張志新(化名)和其女友劉穎柱衔,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愉棱,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡唆铐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奔滑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艾岂。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖朋其,靈堂內(nèi)的尸體忽然破棺而出王浴,到底是詐尸還是另有隱情,我是刑警寧澤梅猿,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布氓辣,位于F島的核電站,受9級特大地震影響袱蚓,放射性物質(zhì)發(fā)生泄漏钞啸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望体斩。 院中可真熱鬧梭稚,春花似錦、人聲如沸硕勿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽源武。三九已至扼褪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粱栖,已是汗流浹背话浇。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留闹究,地道東北人幔崖。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像渣淤,于是被迫代替她去往敵國和親赏寇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容