Runtime源碼剖析---圖解引用計數(shù)與weak

Runtime源碼剖析---圖解引用計數(shù)與weak

在iOS開發(fā)過程中,會經(jīng)常使用到一個修飾詞“weak”,使用場景大家都比較清晰,用于一些對象相互引用的時候,避免出現(xiàn)強引用,對象不能被釋放,出現(xiàn)內(nèi)存泄露的問題。

weak 關(guān)鍵字的作用弱引用,所引用對象的計數(shù)器不會加一,并在引用對象被釋放的時候自動被設(shè)置為 nil。

前言

什么是引用計數(shù)?

  • Objective-C通過retainCount的機制來決定對象是否需要釋放。 每次runloop迭代結(jié)束后,都會檢查對象的 retainCount,如果retainCount等于0,就說明該對象沒有地方需要繼續(xù)使用它,可以被釋放掉了。無論是手動管理內(nèi)存,還是ARC機制,都是通過對retainCount來進行內(nèi)存管理的。
  • 內(nèi)存中每一個對象都有一個屬于自己的引用計數(shù)器。當(dāng)某個對象A被另一個家伙引用時,A的引用計數(shù)器就+1,如果再有一個家伙引用到A,那么A的引用計數(shù)器就再+1。當(dāng)其中某個家伙不再引用A了,A的引用計數(shù)器會-1。直到A的引用計數(shù)減到了0,那么就沒有人再需要它了,就是時候把它釋放掉了。

在引用計數(shù)中,每一個對象負責(zé)維護對象所有引用的計數(shù)值。當(dāng)一個新的引用指向?qū)ο髸r,引用計數(shù)器就遞增,當(dāng)去掉一個引用時,引用計數(shù)就遞減。當(dāng)引用計數(shù)到零時,該對象就將釋放占有的資源。

什么是循環(huán)引用?

  • 循環(huán)引用發(fā)生的條件:

    1. 兩個對象互相強引用對方
    2. 該對象持有其自身(自己引用自己)
  • 這個時候就提出了__weak修飾符,提供弱引用,不能持有對象實例,若該對象被廢棄,則此弱引用將自動失效且處于nil被賦值的狀態(tài)

對前面兩個問題進行總結(jié):

  1. 如何實現(xiàn)的引用計數(shù)管理,控制加一減一和釋放?
  2. 如何維護weak指針防止野指針錯誤?
  • 這也就是我們這篇文章要解決的主要問題,下面我們就會針對上面兩個問題一一解釋。

引用計數(shù)

引用計數(shù)的存儲

有些對象如果支持使用 TaggedPointer,蘋果會直接將其指針值作為引用計數(shù)返回;如果當(dāng)前設(shè)備是 64 位環(huán)境并且使用 Objective-C 2.0,那么‘一些’對象會使用其 isa 指針的一部分空間來存儲它的引用計數(shù);否則 Runtime 會使用一張散列表來管理引用計數(shù)。

  • 我們知道,對于純量類型的變量,是沒有引用計數(shù)的,因為不是對象,其申請的變量存儲在棧上,由系統(tǒng)來負責(zé)管理。

  • 另外一種類型,是前面講過的 Tagged Pointer 小對象,包括 NSNumber、NSString、NSDate 等幾個類的變量。它們也是存儲在棧上,當(dāng)然也沒有引用計數(shù)。

引用計數(shù)存儲方式
  • 關(guān)于對象類型的引用計數(shù),其存在的兩種方式
兩種存儲方式

isa指針中的引用計數(shù)

  • 如果你對isa不夠熟悉的話,可以看看我這篇文章Runtime源碼剖析---圖解對象、類與isa
  • 簡單來說,就是isa再32位之前為指針,在64位操作系統(tǒng)之后就不僅僅是個指針,是個聯(lián)合體,它一部分負責(zé)存儲地址,另一部分可以存儲其他的信息。這樣做可以提高了內(nèi)存的使用率、減少了不必要的開銷。
  • 下面我給出isa的源碼
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    // 相當(dāng)于是unsigned long bits;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    // 這里的定義在isa.h中,如下(注意uintptr_t實際上就是unsigned long)
//    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
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
  • 下面列出isa指針中變量對應(yīng)的含義
變量名 含義
indexed 0 表示普通的 isa 指針,1 表示使用優(yōu)化,存儲引用計數(shù)
has_assoc 表示該對象是否包含 associated object,如果沒有,則析構(gòu)時會更快
has_cxx_dtor 表示該對象是否有 C++ 或 ARC 的析構(gòu)函數(shù),如果沒有,則析構(gòu)時更快
shiftcls 類的指針
magic 固定值為 0xd2,用于在調(diào)試時分辨對象是否未完成初始化。
weakly_referenced 表示該對象是否有過 weak 對象,如果沒有,則析構(gòu)時更快
deallocating 表示該對象是否正在析構(gòu)
has_sidetable_rc 表示該對象的引用計數(shù)值是否過大無法存儲在 isa 指針
extra_rc 存儲引用計數(shù)值減一后的結(jié)果
  • 我們發(fā)現(xiàn)最后兩個變量,就和我們引用計數(shù)相關(guān)。
  • 最后我們用一張圖來看看具體布局
isa布局

Side Table里的引用計數(shù)

  • Side Table 在系統(tǒng)中的結(jié)構(gòu)如下:
Side Table在系統(tǒng)中結(jié)構(gòu)
  • 而每一個Side Table中引用計數(shù)的結(jié)構(gòu)
Side Table中引用計數(shù)結(jié)構(gòu)
  • 現(xiàn)在我們只需要了解一下有這個結(jié)構(gòu)就夠了,我會在后面仔細剖析如何存儲和如何調(diào)用的。

引用計數(shù)的管理

管理引用計數(shù)的方法

管理引用計數(shù)的方法

獲取引用計數(shù)

非ARC環(huán)境下
  • 在非ARC環(huán)境下通過retainCount方法來獲取引用計數(shù)
- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

該方法內(nèi)部調(diào)用了rootRetainCount函數(shù)

inline uintptr_t 
objc_object::rootRetainCount()
{
    //1.如果是Tagged Pointer,直接返回指針
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    //如果是優(yōu)化后的isa
    if (bits.nonpointer) {
        //取出isa中的retainCount+1;
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            //如果Side Table還有存儲,則取出累加返回
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
  • 如果它既不是Tagged Point對象,isa指針中也沒有存儲引用計數(shù),這個時候就會走sidetable_retainCount這個方法。
uintptr_t
objc_object::sidetable_retainCount()
{
    //先從SideTables中取出對應(yīng)的SideTable對象
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    //通過table對象中的refcnts,在通過傳入的地址,找到對應(yīng)的引用計數(shù)
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}
  • 這里有個疑惑,為什么要把取出來的數(shù)據(jù)進行右移位操作呢?
    • 因為在refcnts中存儲的并不是直接就是引用計數(shù),而是可以理解為一個聯(lián)合體,在他的低2位存儲其他特殊信心,引用計數(shù)從第三位開始存儲,所以取出來的時候需要右移操作。
ARC環(huán)境下
  • ARC時代除了使用Core Foundation 庫的 CFGetRetainCount() 方法,也可以使用 Runtime_objc_rootRetainCount(id obj) 方法來獲取引用計數(shù),此時需要引入 <objc/runtime.h> 頭文件。這個函數(shù)也是調(diào)用 objc_objectrootRetainCount() 方法:所以分析同上。
retainCount總結(jié)
獲取引用計數(shù)的方式

retain的實現(xiàn)

inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

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

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
  • 這個方法內(nèi)部會調(diào)用rootRetain() 方法
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    //Tagged Pointer對象直接返回
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    //transcribeToSideTable用于表示extra_rc是否溢出,默認為false
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);//將isa提取出來
        newisa = oldisa;
        //如果isa沒有優(yōu)化,直接從sideTable中返回引用計數(shù)
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        //如果對象正在析構(gòu),則直接返回nil
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        //isa指針中的extra_rc+1;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            //有進位,表示溢出,extra_rc已經(jīng)不能存儲在isa指針中了
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                //如果不處理溢出情況,則在這里會遞歸調(diào)用一次,再進來的時候
                //handleOverflow會被rootRetain_overflow設(shè)置為true,從而直接進入到下面的溢出處理流程
                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.
            /*
                溢出處理
                1.先將extra_rc中引用計數(shù)減半,繼續(xù)存儲在isa中
                2.同時把has_sidetable_rc設(shè)置為true,表明借用了sideTable存儲
                3.將另一半的引用計數(shù),存放在sideTable中
            */
            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)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        //isa的extra_rc溢出,將一半refer count值放到SideTable中
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}
  • 總結(jié):
    • Tagged Pointer 對象,沒有 retain。
    • isa 中 extra_rc 若未溢出,則累加 1。如果溢出,則將 isa 和 side table 對半存儲。
retain操作

release的實現(xiàn)

inline void
objc_object::release()
{
    assert(!isTaggedPointer());

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

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
  • 這個方法內(nèi)部會調(diào)用rootRelease
ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    //如果是Tagged Pointer,直接返回false,不需要dealloc
    if (isTaggedPointer()) return false;
    bool sideTableLocked = false;
    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //如果isa未優(yōu)化,直接調(diào)用sideTable的release函數(shù)
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        //將當(dāng)前isa指針中存儲的引用計數(shù)減1,如果未溢出,返回false,不需要dealloc
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            //減1下導(dǎo)致溢出,則需要從side Table借位,跳轉(zhuǎn)到溢出處理
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    newisa = oldisa;
    if (slowpath(newisa.has_sidetable_rc)) {
        //Side Table存在引用計數(shù)
        if (!handleUnderflow) {
            //如果不處理溢出,此處只會調(diào)用一次,handleUnderflow會被rootRelease_underflow設(shè)置為ture
            //再次進入,則會直接進入溢出處理流程
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }
        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;
        }

        //嘗試從SideTable中取出,當(dāng)前isa能存儲引用計數(shù)最大值的一半的引用計數(shù)  
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
        //如果取出引用計數(shù)大于0
        if (borrowed > 0) {
            //將取出的引用計數(shù)減一,并保存到isa中,保存成功返回false(未被銷毀)
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                //保存到isa中失敗,再次嘗試保存
                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) {
                //如果還是失敗,則將借出來的一半retain count,重新返回給side Table
                //并且重新從2開始嘗試
                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.
        }
    }

    //以上流程執(zhí)行完了,則需要銷毀對象,調(diào)用dealloc
    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;
}
  • release總結(jié)
release操作

dealloc的實現(xiàn)

- (void)dealloc {
    _objc_rootDealloc(self);
}
  • 進入_objc_rootDealloc
void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
  • 進入rootDealloc
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?
        //如果是優(yōu)化的isa
    //并且對應(yīng)的沒有關(guān)聯(lián)對象、weak應(yīng)用、以及c++析構(gòu)函數(shù)和sideTable的引用計數(shù),就直接free掉
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //進入銷毀流程
        object_dispose((id)this);
    }
}
  • 進入object_dispose

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
  • 接著進入objc_destructInstance
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);//清除成員變量
        if (assoc) _object_remove_assocations(obj);//移除關(guān)聯(lián)對象
        obj->clearDeallocating();//將指向當(dāng)前的弱指針置為nil
    }

    return obj;
}
  • 最后我們留下了一個懸念,如何把指向當(dāng)前的弱指針置為nil?我會在weak指針置nil這一節(jié)給大家講解

weak

在最開始我們說引用計數(shù)的存儲方式,清楚說到有兩種,一種是通過isa,另一種是通過Side Tables。那這個東西到底是什么,它又是怎么實現(xiàn)weak指針置nil的呢?接下來我們帶大家慢慢分析

SideTables

  • 我們先介紹幾個知識點

HashMap(哈希表)
基于數(shù)組的一種數(shù)據(jù)結(jié)構(gòu), 通過一定的算法, 把 key 進行運算得出一個數(shù)字, 用這個數(shù)字做數(shù)組下標(biāo), 將 value 存入這個下標(biāo)對應(yīng)的內(nèi)存中.

HashTon(哈希桶)
哈希算法中計算出的數(shù)字, 有可能會重復(fù), 對于哈希值重復(fù)的數(shù)據(jù), 如何存入哈希表呢? 常用方法有閉散列和開散列等方式, 其中采用開散列方式的哈希表, 就可以稱為哈希桶. 開散列就是在哈希值對應(yīng)的位置上, 使用鏈表或數(shù)組, 將哈希值沖突的數(shù)據(jù)存入這個鏈表或者數(shù)組中, 可以提高查找效率.

  • 為了管理所有對象的引用計數(shù)和weak指針,蘋果創(chuàng)建了一個全局的SideTables,雖然名字后面有個"s"不過他其實是一個全局的Hash桶,里面的內(nèi)容裝的都是SideTable結(jié)構(gòu)體而已。它使用對象的內(nèi)存地址當(dāng)它的key。管理引用計數(shù)和weak指針就靠它了。
  • 因為對象引用計數(shù)相關(guān)操作應(yīng)該是原子性的。不然如果多個線程同時去寫一個對象的引用計數(shù),那就會造成數(shù)據(jù)錯亂,失去了內(nèi)存管理的意義。同時又因為內(nèi)存中對象的數(shù)量是非常非常龐大的需要非常頻繁的操作SideTables,所以能對整個Hash表加鎖。蘋果采用了分離鎖技術(shù)。

分離鎖和分拆鎖的區(qū)別
????降低鎖競爭的另一種方法是降低線程請求鎖的頻率。分拆鎖 (lock splitting) 和分離鎖 (lock striping) 是達到此目的兩種方式。相互獨立的狀態(tài)變量,應(yīng)該使用獨立的鎖進行保護。有時開發(fā)人員會錯誤地使用一個鎖保護所有的狀態(tài)變量。這些技術(shù)減小了鎖的粒度,實現(xiàn)了更好的可伸縮性。但是,這些鎖需要仔細地分配,以降低發(fā)生死鎖的危險。
????如果一個鎖守護多個相互獨立的狀態(tài)變量,你可能能夠通過分拆鎖,使每一個鎖守護不同的變量,從而改進可伸縮性。通過這樣的改變,使每一個鎖被請求的頻率都變小了。分拆鎖對于中等競爭強度的鎖,能夠有效地把它們大部分轉(zhuǎn)化為非競爭的鎖,使性能和可伸縮性都得到提高。
????分拆鎖有時候可以被擴展,分成若干加鎖塊的集合,并且它們歸屬于相互獨立的對象,這樣的情況就是分離鎖。

  • SideTables其實就是一個全局的哈希桶
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

SideTables() 方法返回的是一個 StripedMap<SideTable>& 類型的引用:

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }
    ......
};
  • StripedMap 是一個模板類, 內(nèi)部維護一個大小為StripeCount 的數(shù)組, 數(shù)組的成員為結(jié)構(gòu)體 PaddedT, PaddedT 結(jié)構(gòu)體只有一個成員value, value的類型是T.
  • 綜上所述:SideTables() 返回的 StripedMap, 是一個valueSideTable的哈希桶(由于 SideTable內(nèi)部又在維護數(shù)組, 所以這是一個哈希桶結(jié)構(gòu)), 哈希值由對象的地址計算得出.

SideTable

當(dāng)我們通過SideTables[key]來得到SideTable的時候,SideTable的結(jié)構(gòu)如下:

struct SideTable {
    spinlock_t slock;//自旋鎖
    RefcountMap refcnts;//存放引用計數(shù)
    weak_table_t weak_table;//weak_table是一個哈希

    ......
};

spinlock_t:自旋鎖

  • 自旋鎖

自旋鎖比較適用于鎖使用者保持鎖時間比較短的情況。正是由于自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高于互斥鎖。信號量和讀寫信號量適合于保持時間較長的情況,它們會導(dǎo)致調(diào)用者睡眠,因此只能在進程上下文使用,而自旋鎖適合于保持時間非常短的情況,它可以在任何上下文使用。

  • 作用:在操作引用計數(shù)的時候?qū)?code>SideTable加鎖,避免數(shù)據(jù)錯誤

RefcountMap:存放引用計數(shù)

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
  • DenseMap又是一個模版類
template<typename KeyT, typename ValueT,
         bool ZeroValuesArePurgeable = false, 
         typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
    : public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
                          KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable> {
  
  typedef DenseMapBase<DenseMap, KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable> BaseT;
  typedef typename BaseT::BucketT BucketT;
  friend class DenseMapBase<DenseMap, KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable>;

  BucketT *Buckets;
  unsigned NumEntries;
  unsigned NumTombstones;
  unsigned NumBuckets;
};

列舉幾個比較重要的成員變量:

  1. ZeroValuesArePurgeable默認值是false,但 RefcountMap 指定其初始化為true. 這個成員標(biāo)記是否可以使用值為 0 (引用計數(shù)為 1) 的桶. 因為空桶存的初始值就是 0, 所以值為 0 的桶和空桶沒什么區(qū)別. 如果允許使用值為 0 的桶, 查找桶時如果沒有找到對象對應(yīng)的桶, 也沒有找到墓碑桶, 就會優(yōu)先使用值為 0 的桶.
  2. Buckets指針管理一段連續(xù)內(nèi)存空間, 也就是數(shù)組, 數(shù)組成員是BucketT 類型的對象, 我們這里將 BucketT 對象稱為桶(實際上這個數(shù)組才應(yīng)該叫桶, 蘋果把數(shù)組中的元素稱為桶應(yīng)該是為了形象一些, 而不是哈希桶中的桶的意思). 桶數(shù)組在申請空間后, 會進行初始化, 在所有位置上都放上空桶(桶的 keyEmptyKey 時是空桶), 之后對引用計數(shù)的操作, 都要依賴于桶.
  3. umEntries記錄數(shù)組中已使用的非空的桶的個數(shù).
  4. NumTombstones,Tombstone直譯為墓碑, 當(dāng)一個對象的引用計數(shù)為0, 要從桶中取出時, 其所處的位置會被標(biāo)記為 Tombstone.NumTombstones就是數(shù)組中的墓碑的個數(shù). 后面會介紹到墓碑的作用.
  5. NumBuckets桶的數(shù)量, 因為數(shù)組中始終都充滿桶, 所以可以理解為數(shù)組大小

為什么Hash以后還需要個Map?其實蘋果采用的是分塊化的方法。
????舉個例子
????假設(shè)現(xiàn)在內(nèi)存中有16個對象。
0x0000、0x0001、...... 0x000e、0x000f
????咱們創(chuàng)建一個SideTables[8]來存放這16個對象,那么查找的時候發(fā)生Hash沖突的概率就是八分之一。
????假設(shè)SideTables[0x0000]和SideTables[0x0x000f]沖突,映射到相同的結(jié)果。

SideTables[0x0000] == SideTables[0x0x000f]  ==> 都指向同一個SideTable

蘋果把兩個對象的內(nèi)存管理都放到里同一個SideTable中。你在這個SideTable中需要再次調(diào)用table.refcnts.find(0x0000)或者table.refcnts.find(0x000f)來找到他們真正的引用計數(shù)器。
????這里是一個分流。內(nèi)存中對象的數(shù)量實在是太龐大了我們通過第一個Hash表只是過濾了第一次,然后我們還需要再通過這個Map才能精確的定位到我們要找的對象的引用計數(shù)器。

  • 引用計數(shù)結(jié)構(gòu)bucketT實際上是std::pair,類似于isa,使用其中幾個bit來保存引用計數(shù),留出幾個bit用來做其他標(biāo)記位

    1. (1UL<<0) WEAKLY_REFERENCED

      表示是否有弱引用指向這個對象,如果有的話(值為1)在對象釋放的時候需要把所有指向它的弱引用都變成nil(相當(dāng)于其他語言的NULL),避免野指針錯誤。

    2. (1UL<<1)?DEALLOCATING

      表示對象是否正在被釋放。1正在釋放,0沒有。

    3. REAL COUNT

      REAL COUNT的部分才是對象真正的引用計數(shù)存儲區(qū)。所以咱們說的引用計數(shù)加一或者減一,實際上是對整個unsigned long加四或者減四,因為真正的計數(shù)是從2^2位開始的。

    4. (1UL<<(WORD_BITS-1))?SIDE_TABLE其中WORD_BITS

      在32位和64位系統(tǒng)的時候分別等于32和64。隨著對象的引用計數(shù)不斷變大。如果這一位都變成1了,就表示引用計數(shù)已經(jīng)最大了不能再增加了。_RC_PINNED

RefcountMap工作邏輯
  1. 過計算對象地址的哈希值, 來從SideTables中獲取對應(yīng)的SideTable. 哈希值重復(fù)的對象的引用計數(shù)存儲在同一個 SideTable里.
  2. SideTable 使用find() 方法和重載 [] 運算符的方式, 通過對象地址來確定對象對應(yīng)的桶. 最終執(zhí)行到的查找算法是LookupBucketFor().
template<typename LookupKeyT>
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
  const BucketT *BucketsPtr = getBuckets();
  const unsigned NumBuckets = getNumBuckets();

  //如果桶的個數(shù)是0
  if (NumBuckets == 0) {
    FoundBucket = 0;
    return false;//返回false,回上層調(diào)用添加函數(shù)
  }

  // FoundTombstone - Keep track of whether we find a tombstone or zero value while probing.
  const BucketT *FoundTombstone = 0;
  const KeyT EmptyKey = getEmptyKey();
  const KeyT TombstoneKey = getTombstoneKey();
  assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
         !KeyInfoT::isEqual(Val, TombstoneKey) &&
         "Empty/Tombstone value shouldn't be inserted into map!");

  unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);//將哈希值與數(shù)組最大下標(biāo)按位與
  unsigned ProbeAmt = 1;//哈希值重復(fù)的對象需要靠它來重新尋找位置
  
  while (1) {
    const BucketT *ThisBucket = BucketsPtr + BucketNo;//頭指針+下標(biāo),類似于數(shù)組取值
    //找到桶中的key和對象地址相等,則是找到了
    if (KeyInfoT::isEqual(Val, ThisBucket->first)) {
      FoundBucket = ThisBucket;
      return true;
    }
    //找到的桶中的key是空桶占位符,則表示可插入
    if (KeyInfoT::isEqual(ThisBucket->first, EmptyKey)) {
      if (FoundTombstone) ThisBucket = FoundTombstone;//如果曾遇到墓碑,則使用墓碑的位置
      FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;//找到空占位符,則表明表中沒有已經(jīng)插入了該對象的桶
      return false;
    }
    //如果找到了墓碑
    if (KeyInfoT::isEqual(ThisBucket->first, TombstoneKey) && !FoundTombstone)
      FoundTombstone = ThisBucket;  //記錄下墓碑
    //這里涉及到最初定義 typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap, 傳入的第三個參數(shù) true
      //這個參數(shù)代表是否可以清除 0 值, 也就是說這個參數(shù)為 true 并且沒有墓碑的時候, 會記錄下找到的 value 為 0 的桶
    if (ZeroValuesArePurgeable  && 
        ThisBucket->second == 0  &&  !FoundTombstone) 
      FoundTombstone = ThisBucket;

    //用于計數(shù)的ProbeAmt如果大于數(shù)組容量,就會拋出異常
    if (ProbeAmt > NumBuckets) {
      _objc_fatal("Hash table corrupted. This is probably a memory error "
                  "somewhere. (table at %p, buckets at %p (%zu bytes), "
                  "%u buckets, %u entries, %u tombstones, "
                  "data %p %p %p %p)", 
                  this, BucketsPtr, malloc_size(BucketsPtr), 
                  NumBuckets, getNumEntries(), getNumTombstones(), 
                  ((void**)BucketsPtr)[0], ((void**)BucketsPtr)[1], 
                  ((void**)BucketsPtr)[2], ((void**)BucketsPtr)[3]);
    }
    BucketNo += ProbeAmt++;//本次哈希計算得出的下標(biāo)不符合,則利用ProbeAmt尋找下一個下標(biāo)
    BucketNo&= (NumBuckets-1);//得到新的數(shù)字和數(shù)組下標(biāo)按最大值按位與
  }
}

  • 下面我們通過畫圖的方式來展示上述代碼的流程:這里引用小新0541的神作
Buckets數(shù)組

首先我們有一個初始化好的, 大小為 9 的桶數(shù)組, 同時有 a b c d e 五個對象要使用桶數(shù)組, 這里我們假設(shè)五個對象都被哈希算法分配到下標(biāo) 0 的位置里. a 第一個進入, 但 b c d e 由于下標(biāo) 0 處已經(jīng)不是空桶, 則需要進行下一步哈希算法來查找合適的位置, 假設(shè)這 4 個對象又恰巧都被分配到了下標(biāo)為 1 的位置, 但只有 b 可以存入. 假設(shè)每一次哈希計算都只給下標(biāo)增加了 1, 以此類推我們能得到:

5個對象存入后

假設(shè)這個時候 c 對象被釋放了, 之前提到過這個時候會把對應(yīng)的位置的 key 設(shè)置為 TombstoneKey:

c對象釋放后

接下來就體現(xiàn)了墓碑的作用:

  1. 如果 c 對象銷毀后將下標(biāo) 2 的桶設(shè)置為空桶, 此時為 e 對象增加引用計數(shù), 根據(jù)哈希算法查找到下標(biāo)為 2 的桶時, 就會直接插入, 無法為已經(jīng)在下標(biāo)為 4 的桶中的 e 增加引用計數(shù).
  2. 如果此時初始化了一個新的對象 f, 根據(jù)哈希算法查找到下標(biāo)為 2 的桶時發(fā)現(xiàn)桶中放置了墓碑, 此時會記錄下來下標(biāo) 2. 接下來繼續(xù)哈希算法查找位置, 查找到空桶時, 就證明表中沒有對象 f, 此時 f 使用記錄好的下標(biāo) 2 的桶而不是查找到的空桶, 就可以利用到已經(jīng)釋放的位置.
  • 插入對象引用計數(shù)流程
BucketT *InsertIntoBucketImpl(const KeyT &Key, BucketT *TheBucket) {
  //如果哈希表的負載大于3/4,則擴展該表。
    //如果只有不到1/8的桶是空的(這意味著許多桶都是由tombstone填充的),則重新哈希表而不增加。
    //后一種情況比較棘手。例如,如果我們有一個空桶,有大量的tombstone,失敗的查找(例如插入)將不得不探查幾乎整個表,直到它找到空桶為止。如果表中完全填滿了tombstone,則不會成功進行任何查找,從而導(dǎo)致無限循環(huán)的查找。
  unsigned NewNumEntries = getNumEntries() + 1;//桶的使用量+1
  unsigned NumBuckets = getNumBuckets();//桶的總數(shù)
  if (NewNumEntries*4 >= NumBuckets*3) {//使用量超過3/4
    this->grow(NumBuckets * 2);//數(shù)組大小*2做參數(shù),grow中會決定具體數(shù)值
    //grow中會重新布置所有桶的位置,所以將要插入的對象也要重新定位
    LookupBucketFor(Key, TheBucket);
    NumBuckets = getNumBuckets();//獲取最新數(shù)組的大小
  }
  //如果空桶數(shù)量小于1/8,哈希查找會很難定位到空桶的位置
  if (NumBuckets-(NewNumEntries+getNumTombstones()) <= NumBuckets/8) {
    //grow以原大小重新開辟空間,重新安排桶的位置并能清楚墓碑
    this->grow(NumBuckets);
    LookupBucketFor(Key, TheBucket);//重新布局后將要插入的對象也要重新確定位置
  }
  assert(TheBucket);
    //找到的BucketT標(biāo)記了EmptyKey,可以直接使用
  if (KeyInfoT::isEqual(TheBucket->first, getEmptyKey())) {
    //桶數(shù)量+1
    incrementNumEntries();      
  }
  //如果找的是墓碑
  else if (KeyInfoT::isEqual(TheBucket->first, getTombstoneKey())) {
    incrementNumEntries();//桶的使用量+1
    decrementNumTombstones();//墓碑?dāng)?shù)量-1
  }
  //如果找到的位置是value為0的位置
  else if (ZeroValuesArePurgeable  &&  TheBucket->second == 0) {
    // Purging a zero. No accounting changes.
    TheBucket->second.~ValueT();
  } else {
    // Updating an existing entry. No accounting changes.
  }

  return TheBucket;
}

weak_table_t:維護weak指針

  • weak_table是一個哈希表的結(jié)構(gòu), 根據(jù)weak指針指向的對象的地址計算哈希值, 哈希值相同的對象按照下標(biāo) +1 的形式向后查找可用位置, 是典型的閉散列算法
struct weak_table_t {
    weak_entry_t *weak_entries;//連續(xù)地址空間的頭指針,數(shù)組
    size_t    num_entries;//數(shù)組中已經(jīng)占用位置的個數(shù)
    uintptr_t mask;//數(shù)組下標(biāo)最大值(即數(shù)組大小-1)
    uintptr_t max_hash_displacement;//最大哈希偏移值
};
weak_entry_t
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;//對象的地址
    union {//這里是一個聯(lián)合體
        struct {
            //因為這里要存儲的又是一個weak指針數(shù)組,所以采用哈希算法
            weak_referrer_t *referrers;//指向referent對象的weak指針數(shù)組
            uintptr_t        out_of_line_ness : 2;//這里標(biāo)記是否超過內(nèi)聯(lián)邊界
            uintptr_t        num_refs : PTR_MINUS_2;//數(shù)組中已占用的大小
            uintptr_t        mask;//數(shù)組下標(biāo)最大值(數(shù)組大小-1)
            uintptr_t        max_hash_displacement;//最大哈希偏移值
        };
        struct {
            //這是一個取名急哦啊內(nèi)聯(lián)引用的數(shù)組
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];//宏定義的值是4
        };
    };
    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }
    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }
    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};
  • 我們通過對象的地址, 可以在weak_table_t 中找到對應(yīng)的weak_entry_t,weak_entry_t中保存了所有指向這個對象的 weak指針.
  • 當(dāng)指向這個對象的 weak 指針不超過 4 個, 則直接使用數(shù)組 inline_referrers, 省去了哈希操作的步驟, 如果 weak 指針個數(shù)超過了 4 個, 就要使用第一個結(jié)構(gòu)體中的哈希表.
  • 下面我們用一張圖來看看,weak_table表是什么樣的結(jié)構(gòu)
weak_table
  • 接著我們再來看看SideTables整體結(jié)構(gòu)
SideTables整體結(jié)構(gòu)
SideTables存儲結(jié)構(gòu)
weak_table_t 的工作邏輯
初始化weak指針
  1. ARC下, 編譯器會自動添加管理引用計數(shù)的代碼,weak 指針賦值的時候, 編譯器會調(diào)用 storeWeak來賦值, 若weak指針有指向的對象, 那么會先調(diào)用 weak_unregister_no_lock() 方法來從原有的表中先刪除這個 weak 指針, 然后再調(diào)用 weak_register_no_lock() 來向?qū)?yīng)的表中插入這個 weak 指針.
id
objc_initWeak(id *location, id newObj)
{
    //查看對象實例是否有效
    //無效對象直接導(dǎo)致指針釋放
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}
  • 通過上面方法進行給weak指針賦值,最終會調(diào)用storeWeak
// HaveOld:  true - 變量有值
//          false - 需要被及時清理,當(dāng)前值可能為 nil
// HaveNew:  true - 需要被分配的新值,當(dāng)前值可能為 nil
//          false - 不需要分配新值
// CrashIfDeallocating: true - 說明 newObj 已經(jīng)釋放或者 newObj 不支持弱引用,該過程需要暫停
//          false - 用 nil 替代存儲
template <HaveOld haveOld, HaveNew haveNew, CrashIfDeallocating crashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);
    
    // 該過程用來更新弱引用指針的指向
    // 初始化 previouslyInitializedClass 指針
    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    //模板函數(shù), haveOld 和 haveNew 由編譯器決定傳入的值, location 是 weak 指針, newObj 是 weak 指針將要指向的對象
 retry:
    if (haveOld) {
        //更改指針,獲得以oldObj 為索引所存儲的值地址
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        // 更改新值指針,獲得以 newObj 為索引所存儲的值地址
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
        // 加鎖操作,防止多線程中競爭沖突
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
        // 避免線程沖突重處理
    // location 應(yīng)該與 oldObj 保持一致,如果不同,說明當(dāng)前的 location 已經(jīng)處理過 oldObj 可是又被其他線程所修改
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }
        // 防止弱引用間死鎖
    // 并且通過 +initialize 初始化構(gòu)造器保證所有弱引用的 isa 非空指向
    if (haveNew  &&  newObj) 
        // 獲得新對象的 isa 指針
        Class cls = newObj->getIsa();
              // 判斷 isa 非空且已經(jīng)初始化
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            // 對其 isa 指針進行初始化
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
                        // 如果該類已經(jīng)完成執(zhí)行 +initialize 方法是最理想情況
            // 如果該類 +initialize 在線程中
            // 例如 +initialize 正在調(diào)用 storeWeak 方法
            // 需要手動對其增加保護策略,并設(shè)置 previouslyInitializedClass 指針進行標(biāo)記
            previouslyInitializedClass = cls;
                        // 重新嘗試
            goto retry;
        }
    }

    if (haveOld) {
        //如果weak指針有舊值,則需要在weak_table中處理掉舊值
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
            
    if (haveNew) {
        //如果weak指針將要指向新值,在weak_table中處理賦值操作
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // 如果弱引用被釋放 weak_register_no_lock 方法返回 nil
        // 在引用計數(shù)表中設(shè)置若引用標(biāo)記位
        if (newObj  &&  !newObj->isTaggedPointer()) {
            // 弱引用位初始化操作
            // 引用計數(shù)那張散列表的weak引用對象的引用計數(shù)中標(biāo)識為weak引用
            newObj->setWeaklyReferenced_nolock();
        }
        // 之前不要設(shè)置 location 對象,這里需要更改指針指向
        *location = (id)newObj;
    }
    else {
        // 沒有新值,則無需更改
    }
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
    return (id)newObj;
}
  • 如果weak指針有舊值,則需要在weak_table中處理掉舊值,就需要用到weak_unregister_no_lock方法
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)
{
    objc_object *referent = (objc_object *)referent_id;//weak指針指向的對象
    objc_object **referrer = (objc_object **)referrer_id;//referrer_id是weak指針,操作時需要用到這個指針
    weak_entry_t *entry;
    if (!referent) return;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {//查找referent對象對應(yīng)的entry
        remove_referrer(entry, referrer);//從referent 對應(yīng)的 entry 中刪除地址為 referrer 的 weak 指針
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {//如果 entry 中的數(shù)組容量大于 4 并且數(shù)組中還有元素
            empty = false;//entry 非空
        } else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {//否則循環(huán)查找 entry 數(shù)組, 如果 4 個位置中有一個非空
                    empty = false; //entry 非空
                    break;
                }
            }
        }
        if (empty) {//如果沒有通過之前的查找邏輯, 則說明 entry 為空
            weak_entry_remove(weak_table, entry);//從 weak_table 中移除該條 entry
        }
    }
}

//接著會調(diào)用weak_entry_remove方法
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    if (entry->out_of_line()) free(entry->referrers); //如果 out_of_line(), 則需要 free 掉為 referrers alloc 的空間
    bzero(entry, sizeof(*entry));//entry 所屬空間清空
    weak_table->num_entries--;//weak_table 中 entries 元素個數(shù) -1
    weak_compact_maybe(weak_table);//根據(jù)需要重新調(diào)整 weak_table 的空間
}

//如何重新調(diào)整weak_table空間
static void weak_compact_maybe(weak_table_t *weak_table) {
    size_t old_size = TABLE_SIZE(weak_table);
    //如果數(shù)組大小大于 1024, 但使用量小于 1/16 的話, 將數(shù)組進行收縮, 節(jié)省空間
    if (old_size >= 1024  && old_size / 16 >= weak_table->num_entries) {
        weak_resize(weak_table, old_size / 8); //收縮至原有大小的 1/8
        //使用量小于 1/16, 收縮至 1/8 后, 使用量小于 1/2
    }
}
  • 如果weak指針將要指向新值,在weak_table中處理賦值操作,需要運用weak_register_no_lock方法
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;
    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }

    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }
  
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {//如果 weak_table 有對應(yīng)的 entry
        append_referrer(entry, referrer);//將 weak 指針存入對應(yīng)的 entry 中
    } 
    else {
        weak_entry_t new_entry(referent, referrer);//創(chuàng)建新的 entry
        weak_grow_maybe(weak_table);//查看是否需要調(diào)整 weak_table 中 weak_entries 數(shù)組大小
        weak_entry_insert(weak_table, &new_entry);//將新的 entry 插入到 weak_table 中
    }
    return referent_id;
}

//通過調(diào)用append_referrer()方法將weak存入entry
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if (! entry->out_of_line()) {//如果數(shù)組大小沒超過 4
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {//循環(huán)查找數(shù)組成員
                entry->inline_referrers[i] = new_referrer;//把新的 weak 指針插入到空位置
                return;
            }
        }
        //數(shù)組中的 4 個位置都非空, 就要調(diào)整策略使用 referrers 了
        //從這里開始, 這一段是把 inline_referrers 數(shù)組調(diào)整為使用 referrers 的形式
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));//還是開辟 4 個 weak_referrer_t 大小的空間
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];//將 inline_referrers 中的值賦值給 referrers
        }
        //配置 entry 結(jié)構(gòu)
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    assert(entry->out_of_line());
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {//數(shù)組使用量超過 3/4
        return grow_refs_and_insert(entry, new_referrer);//需要擴展數(shù)組并進行插入
    }
    //開始哈希算法    
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;//使用哈希算法計算到一個起始下標(biāo)
    size_t hash_displacement = 0;//哈希偏移次數(shù)
    while (entry->referrers[index] != nil) {//循環(huán)找空位置
        hash_displacement++;//移位一次 +1
        index = (index+1) & entry->mask;//從起始位置開始遍歷, 對數(shù)組大小取模
        if (index == begin) bad_weak_table(entry);//如果找了一圈, 證明算法出了點問題
    }
    //這里記錄下移位的最大值, 那么數(shù)組里的任何一個數(shù)據(jù), 存儲時的移位次數(shù)都不大于這個值
    //可以提升查找時的效率, 如果移位次數(shù)超過了這個值都沒有找到, 就證明要查找的項不在數(shù)組中
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;//數(shù)組使用量 +1
}

  • 下面我們用一張圖來總結(jié)初始化weak指針的流程
初始化weak流程
weak指針置nil

當(dāng)weak引用指向的對象被釋放時,我們需要把指針置為nil

  • 我們在前面已經(jīng)講解了,當(dāng)一個對象釋放時,需要調(diào)用objc_release方法,如果引用計數(shù)為0時,會執(zhí)行dealloc方法,在把weak指針置nil的過程會調(diào)用clearDeallocating
void 
objc_clear_deallocating(id obj) 
{
    assert(obj);

    if (obj->isTaggedPointer()) return;
    obj->clearDeallocating();
}
  • 在函數(shù)內(nèi)部調(diào)用了clearDeallocating函數(shù)
inline void 
objc_object::clearDeallocating()
{
    //如果是沒有優(yōu)化過的isa指針
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    //是優(yōu)化過的isa指針,并且是否有弱引用過,或者引用計數(shù)是否存在sideTable中
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }
    assert(!sidetable_present());
}
  • 不論我們是哪種方式,我們都會調(diào)用weak_clear_no_lock方法
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //1、拿到被銷毀對象的指針
    objc_object *referent = (objc_object *)referent_id;
    //2、通過 指針 在weak_table中查找出對應(yīng)的entry
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        return;
    }

    //3、將所有的引用設(shè)置成nil
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        //3.1、如果弱引用超過4個則將referrers數(shù)組內(nèi)的弱引用都置成nil。
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        //3.2、不超過4個則將inline_referrers數(shù)組內(nèi)的弱引用都置成nil
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    //循環(huán)設(shè)置所有的引用為nil
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    //4、從weak_table中移除entry
    weak_entry_remove(weak_table, entry);
}
?著作權(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)容

  • 準(zhǔn)備工作 請準(zhǔn)備好750.1版本的objc4源碼一份【目前最新的版本】,打開它,找到文章中提到的方法,類型,對象 ...
    薩繆閱讀 731評論 1 6
  • 通過以下方法查看iOS的引用計數(shù)管理: alloc retain release retainCount deal...
    fou7閱讀 817評論 0 4
  • 在 Objective-C 2.0 中,我們無需手動進行內(nèi)存管理,因為ARC會自動幫我們在編譯的時候,在合適的地方...
    kikido閱讀 653評論 0 1
  • 2. 第二篇是引用 iOS入門級攻城尸 的2篇文章 原文如下: 一、引用計數(shù)的概念 這一部分是寫給非iOS工程師的...
    roger_Hunter閱讀 2,470評論 0 2
  • 1、內(nèi)存布局 stack:方法調(diào)用 heap:通過alloc等分配對象 bss:未初始化的全局變量等。 data:...
    AKyS佐毅閱讀 1,715評論 0 19

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