詳解獲取weak對象的過程

答案

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

為什么我會有這個疑問?

最近復(fù)習了OC內(nèi)存管理的相關(guān)知識,在查閱相關(guān)知識時,看到了這篇文章weak指針的線程安全和自動置nil的深度探討,作者提出了一個下面這個比較有意思的問題?

weak指針會自動置為nil的原因就是在一個對象的delloc中會去弱引用表里面查找所存儲weak指針的數(shù)組,然后去遍歷置為nil。相信這個結(jié)論家都比較認同。但是,如果一個類重寫delloc方法,且設(shè)置為MRC并不調(diào)用super delloc。也就是說這個類必定不能順利的完成delloc,并不能把指針置為nil,但是當獲取weak指針的時候,weak指針卻神奇地為nil。難道之前的結(jié)論是錯誤的?

對于這個問題,作者給出的答案是:獲取weak的指向為nil,其真是的弱引用表可能沒有清空,或者正在被清空,但我們?nèi)≈祑eak指針地值是nil,始作俑者是objc_loadWeakRetained方法。會直接返回nil給我們使用,其真正的弱指針還是存在的,還是指向該對象的。在runtime源碼里面追蹤retainWeakReference地實現(xiàn),最終來的了objc_object::rootRetain函數(shù),猜想:應(yīng)該是isa指針的是否正在被delloc的位域起了作用。如果一個對象被標記為正在被delloc,那么獲取其weak指針會被直接返回nil。與其weak指針的真身無關(guān)。

網(wǎng)上分析objc_loadWeakRetained源碼的文章比較多,這里只貼出來,就不分析了,

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.png
  1. 獲取weak指針時,會調(diào)用objc_loadWeakRetained
  2. 判斷weak指向的對象是否是isTaggedPointer,若是:直接返回該對象
  3. 判斷ISA->hasCustomRR(),我與作者的分歧就在著,此比特位會在該類或父類重寫下列方法時retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference返回true,一般情況我們都不會重寫這些方法,因此會返回false,取反就為true
  4. 那么下一步就會執(zhí)行if (! obj->rootTryRetain()) { result = nil; },嘗試對該對象進行retain
  5. 若retain成功則返回該對象
  6. 若retain失敗,則會返回nil,

obj->rootTryRetain(),這個方法最終會調(diào)用objc_object::rootRetain(bool tryRetain, bool handleOverflow),并且參數(shù)為true,false,在這個函數(shù)里有一下判斷:當tryRetain為true,并且對象為被標記為deallocating時,會返回nil

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

當然作者的那個答案也沒有錯,是因為作者重寫了retainWeakReference方法,讓hasCustomRR為true,會走下面的else,并且作者把retainWeakReference直接返回了YES,那么最終會返回該對象。只是該對象被標記為deallocating,并沒有真正的被釋放。

這個時候我又產(chǎn)生了一個新的疑問,既然獲取weak修飾的對象,會調(diào)用objc_loadWeakRetained方法,而此方法最終又會調(diào)用rootRetain對該對象的引用計數(shù)器加1,那么什么時候?qū)υ搶ο蟮囊糜嫈?shù)器減1的呢?

創(chuàng)建一個可調(diào)試的objc-runtime的工程,在rootRetain方法中添加打印語句printf("rootRetain\n");,在rootRelease方法中添加打印語句printf("rootRelease\n");,然后測試下面代碼

@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;
}

打印結(jié)果如下


屏幕快照 2019-12-10 下午11.23.34.png

針對結(jié)果,每次操作weak指向的對象都會對該對象進行一次retain和release,retain是在objc_loadWeakRetained中經(jīng)過層層調(diào)用rootRetain方法添加的,那release如何調(diào)用的呢?

這時候分為2種情況:

  1. 在ARC中編譯器會在weak對象操作的下面插入一條release語句,weakPerson1.age = 18;相當于weakPerson1.age = 18;object_release(obj)
  2. 在MRC中,獲取weak指向的對象時,并不會直接調(diào)用objc_loadWeakRetained,而是會調(diào)用objc_loadWeak,此方法的實現(xiàn)如下:利用自動釋放池,對retain的對象進行release操作
id objc_loadWeak(id *location) {
    if (!*location) return nil;
    return objc_autorelease(objc_loadWeakRetained(location));
}

關(guān)于weak的另一個問題

在提出問題之前,你首先要了解weak_table_tweak_entry_t以及weak的基本原理。我們知道weak_entry_t是一個存放著某個對象所有的弱引用列表,是一個類數(shù)組對象。那么下面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在存儲的弱引用的個數(shù)小于4的時候,使用的是內(nèi)聯(lián)數(shù)組,直接初始化了4個位置,也就是說當小于4時,長度會一直是4,每次需要刪除某一個弱引用時,都會對數(shù)組進行遍歷,查找到該引用進行置nil,需要添加時,會遍歷此數(shù)組,看有沒有為nil的,若有,就代表有空位,就賦值,若沒有就代表此內(nèi)聯(lián)的數(shù)組已經(jīng)滿了,此時會把內(nèi)聯(lián)數(shù)組轉(zhuǎn)成哈希表,哈希表的默認長度為8.weak_entry_tout_of_line用來標記是否使用內(nèi)聯(lián)數(shù)組。

參考

  1. 從源碼角度看蘋果是如何實現(xiàn) retainCount、retain 和 release 的
  2. Xcode 10 下如何創(chuàng)建可調(diào)試的objc4-723、objc4-750.1工程
  3. weak指針的線程安全和自動置nil的深度探討
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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