詳解獲取weak對象的過程

答案

這裏假設,此對象不是TaggedPointer對象,除了一些必要的判斷外,在ARC中,獲取weak指針時,會調用objc_loadWeakRetained,此方法最終會調用objc_object::rootRetain,對該對象的引用計數器加1,而後在此條語句的下面插入一條release語句,對引用計數器減1,在MRC中,會調用objc_autorelease(objc_loadWeakRetained(location));,利用objc_autorelease對引用計數器減1.c#

爲何我會有這個疑問?

最近複習了OC內存管理的相關知識,在查閱相關知識時,看到了這篇文章weak指針的線程安全和自動置nil的深度探討,做者提出了一個下面這個比較有意思的問題?數組

weak指針會自動置爲nil的緣由就是在一個對象的delloc中會去弱引用表裏面查找所存儲weak指針的數組,而後去遍歷置爲nil。相信這個結論家都比較認同。可是,若是一個類重寫delloc方法,且設置爲MRC並不調用super delloc。也就是說這個類一定不能順利的完成delloc,並不能把指針置爲nil,可是當獲取weak指針的時候,weak指針卻神奇地爲nil。難道以前的結論是錯誤的?安全

對於這個問題,做者給出的答案是:獲取weak的指向爲nil,其真是的弱引用表可能沒有清空,或者正在被清空,但咱們取值weak指針地值是nil,始做俑者是objc_loadWeakRetained方法。會直接返回nil給咱們使用,其真正的弱指針仍是存在的,仍是指向該對象的。在runtime源碼裏面追蹤retainWeakReference地實現,最終來的了objc_object::rootRetain函數,猜測:應該是isa指針的是否正在被delloc的位域起了做用。若是一個對象被標記爲正在被delloc,那麼獲取其weak指針會被直接返回nil。與其weak指針的真身無關。bash

網上分析objc_loadWeakRetained源碼的文章比較多,這裏只貼出來,就不分析了,ide

id objc_loadWeakRetained(id *location) {
    id obj;id result;Class cls;
    SideTable *table;
 retry:
    obj = *location;
    if (!obj) return nil;
    if (obj->isTaggedPointer()) return obj;
    table = &SideTables()[obj];
    table->lock();
    if (*location != obj) {
        table->unlock();
        goto retry;
    }
    result = obj;
    cls = obj->ISA();
    if (!cls->hasCustomRR()) {
        if (! obj->rootTryRetain()) {
            result = nil;
        }
    }
    else {
        if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) {
            BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL))
                class_getMethodImplementation(cls, SEL_retainWeakReference);
            if ((IMP)tryRetain == _objc_msgForward) {
                result = nil;
            }
            else if (! (*tryRetain)(obj, SEL_retainWeakReference)) {
                result = nil;
            }
        }
        else {
            table->unlock();
            _class_initialize(cls);
            goto retry;
        }
    }
    table->unlock();
    return result;
}
複製代碼

對於做者的答案我是認同的,不過我認爲做者那麼說不全面,我認爲的流程是這樣的函數

屏幕快照 2019-12-11 上午12.10.19

  1. 獲取weak指針時,會調用objc_loadWeakRetained
  2. 判斷weak指向的對象是不是isTaggedPointer,如果:直接返回該對象
  3. 判斷ISA->hasCustomRR(),我與做者的分歧就在着,此比特位會在該類或父類重寫下列方法時retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference返回true,通常狀況咱們都不會重寫這些方法,所以會返回false,取反就爲true
  4. 那麼下一步就會執行if (! obj->rootTryRetain()) { result = nil; },嘗試對該對象進行retain
  5. 若retain成功則返回該對象
  6. 若retain失敗,則會返回nil,

obj->rootTryRetain(),這個方法最終會調用objc_object::rootRetain(bool tryRetain, bool handleOverflow),而且參數爲true,false,在這個函數裏有一下判斷:當tryRetain爲true,而且對象爲被標記爲deallocating時,會返回nilpost

if (slowpath(tryRetain && newisa.deallocating)) {
    ClearExclusive(&isa.bits);
    if (!tryRetain && sideTableLocked) sidetable_unlock();
    return nil;
}
複製代碼

固然做者的那個答案也沒有錯,是由於做者重寫了retainWeakReference方法,讓hasCustomRR爲true,會走下面的else,而且做者把retainWeakReference直接返回了YES,那麼最終會返回該對象。只是該對象被標記爲deallocating,並無真正的被釋放。測試

這個時候我又產生了一個新的疑問,既然獲取weak修飾的對象,會調用objc_loadWeakRetained方法,而此方法最終又會調用rootRetain對該對象的引用計數器加1,那麼何時對該對象的引用計數器減1的呢?ui

建立一個可調試的objc-runtime的工程,在rootRetain方法中添加打印語句printf("rootRetain\n");,在rootRelease方法中添加打印語句printf("rootRelease\n");,而後測試下面代碼this

@interface Person : NSObject
@property (nonatomic, assign)NSInteger age;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __weak Person *weakPerson1;
        Person *obj = [[Person alloc]init];
        weakPerson1 = obj;
        weakPerson1.age = 18;
        printf("第一次retain-release\n");
        weakPerson1.age = 18;
        printf("第二次retain-release\n");
        weakPerson1.age = 18;
        printf("第三次retain-release\n");
        weakPerson1.age = 18;
        printf("第四次retain-release\n4");
        NSLog(@"hello world");
    }
    return 0;
}
複製代碼

打印結果以下

屏幕快照 2019-12-10 下午11.23.34
針對結果,每次操做weak指向的對象都會對該對象進行一次retain和release,retain是在 objc_loadWeakRetained中通過層層調用 rootRetain方法添加的,那release如何調用的呢?

這時候分爲2種狀況:

  1. 在ARC中編譯器會在weak對象操做的下面插入一條release語句,weakPerson1.age = 18;至關於weakPerson1.age = 18;object_release(obj)
  2. 在MRC中,獲取weak指向的對象時,並不會直接調用objc_loadWeakRetained,而是會調用objc_loadWeak,此方法的實現以下:利用自動釋放池,對retain的對象進行release操做
id objc_loadWeak(id *location) {
    if (!*location) return nil;
    return objc_autorelease(objc_loadWeakRetained(location));
}
複製代碼

關於weak的另外一個問題

在提出問題以前,你首先要了解weak_table_tweak_entry_t以及weak的基本原理。咱們知道weak_entry_t是一個存放着某個對象全部的弱引用列表,是一個類數組對象。那麼下面2種狀況,weak_entry_t的長度分別是多少? 第一種狀況:

__weak Person *weakPerson1;
    @autoreleasepool {
        Person *obj = [[Person alloc]init];
        weakPerson1 = obj;
    }
    // 在此時,對象obj會被釋放,在釋放的過程會把全部指向它的弱引用都置爲nil
    // 此時存儲弱引用的weak_entry_t的長度是多少
複製代碼

第二種狀況:

__weak Person *weakPerson1;
    __weak Person *weakPerson2;
    __weak Person *weakPerson3;
    __weak Person *weakPerson4;
    __weak Person *weakPerson5;
    @autoreleasepool {
        Person *obj = [[Person alloc]init];
        weakPerson1 = obj;
        weakPerson2 = obj;
        weakPerson3 = obj;
        weakPerson4 = obj;
        weakPerson5 = obj;
    }
    // 在此時,對象obj會被釋放,在釋放的過程會把全部指向它的弱引用都置爲nil
    // 此時存儲弱引用的weak_entry_t的長度是多少
    return 0;
複製代碼

答案是,第一種狀況是4,第二種狀況是8,緣由是weak_entry_t在存儲的弱引用的個數小於4的時候,使用的是內聯數組,直接初始化了4個位置,也就是說當小於4時,長度會一直是4,每次須要刪除某一個弱引用時,都會對數組進行遍歷,查找到該引用進行置nil,須要添加時,會遍歷此數組,看有沒有爲nil的,如有,就表明有空位,就賦值,若沒有就表明此內聯的數組已經滿了,此時會把內聯數組轉成哈希表,哈希表的默認長度爲8.weak_entry_tout_of_line用來標記是否使用內聯數組。

參考

  1. 從源碼角度看蘋果是如何實現 retainCount、retain 和 release 的
  2. Xcode 10 下如何建立可調試的objc4-72三、objc4-750.1工程
  3. weak指針的線程安全和自動置nil的深度探討
  4. 理解 ARC 實現原理
相關文章
相關標籤/搜索