1. 程式人生 > >黑箱中的 retain 和 release

黑箱中的 retain 和 release

ntp ase obj 保存 最簡版 重新 root storage 發送

https://github.com/Draveness/Analyze/blob/master/contents/objc/黑箱中的%20retain%20和%20release.md

寫在前面

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

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

從 retain 開始

如今我們已經進入了全面使用 ARC 的時代,幾年前還經常使用的 retainrelease

方法已經很難出現於我們的視野中了,絕大多數內存管理的實現細節都由編譯器代勞。

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

下面是 retain 方法的調用棧:

- [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 方法回收對象

有關於自動釋放池實現的介紹,可以看自動釋放池的前世今生。



作者:Draveness
鏈接:http://www.jianshu.com/p/fff25c76aba4
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

黑箱中的 retain 和 release