黑箱中的 retain 和 release

關注倉庫,及時得到更新:iOS-Source-Code-Analyzegit

Follow: Draveness · Githubgithub

因爲 Objective-C 中的內存管理是一個比較大的話題,因此會分爲兩篇文章來對內存管理中的一些機制進行剖析,一部分分析自動釋放池以及 autorelease 方法,另外一部分分析 retainrelease 方法的實現以及自動引用計數。ide

寫在前面

在接口設計時,咱們常常要考慮某些意義上的平衡。在內存管理中也是這樣,Objective-C 同時爲咱們提供了增長引用計數的 retain 和減小引用計數的 release 方法。this

這篇文章會在源代碼層面介紹 Objective-C 中 retainrelease 的實現,它們是如何達到平衡的。spa

從 retain 開始

現在咱們已經進入了全面使用 ARC 的時代,幾年前還常用的 retainrelease 方法已經很難出現於咱們的視野中了,絕大多數內存管理的實現細節都由編譯器代勞。設計

在這裏,咱們還要從 retain 方法開始,對內存管理的實現細節一探究竟。指針

下面是 retain 方法的調用棧:code

- [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

  1. 使用 LoadExclusive 加載 isa 的值

  2. 調用 addc(newisa.bits, RC_ONE, 0, &carry) 方法將 isa 的值加一

  3. 調用 StoreExclusive(&isa.bits, oldisa.bits, newisa.bits) 更新 isa 的值

  4. 返回當前對象

有進位版本的 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_rcSideTable 來存儲某個對象的自動引用計數。

更重要的是,若是自動引用計數爲 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;
}
  1. 使用 LoadExclusive 獲取 isa 內容

  2. isa 中的引用計數減一

  3. 調用 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 返回的值

這也就證實了咱們以前獲得的結論。

小結

咱們在這篇文章中已經介紹了 retainrelease 這一對用於內存管理的方法是如何實現的,這裏總結一下文章一下比較重要的問題。

  • extra_rc 只會保存額外的自動引用計數,對象實際的引用計數會在這個基礎上 +1

  • Objective-C 使用 isa 中的 extra_rcSideTable 來存儲對象的引用計數

  • 在對象的引用計數歸零時,會調用 dealloc 方法回收對象

有關於自動釋放池實現的介紹,能夠看自動釋放池的前世此生

關注倉庫,及時得到更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

相關文章
相關標籤/搜索