iOS進階專項分析(十)魁亦、iOS內(nèi)存的布局管理及優(yōu)化

對于學習來說渔隶,最大的成本不是金錢羔挡,而是時間。低質(zhì)量低效率的學習不僅是對金錢的浪費间唉,更是對時間绞灼、生命的浪費。

先來看一系列大廠必問的iOS的高階面試題:

  1. 什么是ARC&MRC?底層是如何實現(xiàn)的呈野?
  2. 對象調(diào)用alloc和init方法之后低矮,引用計數(shù)是0還是1?為什么被冒?
  3. weak實現(xiàn)原理以及weak指針是怎樣移除的军掂?何時移除?

然后帶著這三個問題開始本篇干貨:

  1. iOS內(nèi)存布局及優(yōu)化技巧
  2. 內(nèi)存管理機制ARC&MRC
  3. 內(nèi)存管理之引用計數(shù)retain/release底層實現(xiàn)
  4. 內(nèi)存管理之dealloc底層實現(xiàn)
  5. 內(nèi)存管理之weak底層實現(xiàn)

一昨悼、內(nèi)存布局及優(yōu)化


三圖看懂內(nèi)存布局及優(yōu)化

1.內(nèi)存布局及存儲類型

1.1內(nèi)存布局.png

1.2內(nèi)存布局.png

2.內(nèi)存布局方向的優(yōu)化技巧

1.3內(nèi)存優(yōu)化.png

二蝗锥、內(nèi)存管理機制ARC&MRC


1、內(nèi)存管理機制:

引用計數(shù)機制率触,創(chuàng)建時引用計數(shù)為1终议,被持有會對引用計數(shù)+1,對象不再使用或者手動release會對引用計數(shù)-1葱蝗,當引用計數(shù)為0的時候由系統(tǒng)進行銷毀穴张。(注意釋放的條件,不要和release混淆两曼,release只是引用計數(shù)-1皂甘,而不是釋放)

引用計數(shù)管理機制:誰創(chuàng)建,誰釋放悼凑;誰引用叮贩,誰管理击狮。

2、MRCARC的異同:

MRC(全稱Manual Reference Counting 手動引用計數(shù))和ARC(全稱Automatic Reference Counting益老,自動引用計數(shù)彪蓬,iOS5推出)底層都是引用計數(shù)機制。

ARC是編譯屬性捺萌,是編譯器和runtime結(jié)合(對象的持有和釋放)實現(xiàn)的結(jié)果档冬。

三、內(nèi)存管理之引用計數(shù)retain/release底層實現(xiàn)


打開Objc源碼桃纯,在objc-object.h中找到這倆的實現(xiàn)

retain的實現(xiàn):

// Equivalent to calling [this retain], with shortcuts if there is no override
inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

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

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

release的實現(xiàn)部分:

// Equivalent to calling [this release], with shortcuts if there is no override
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

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

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}

明顯看出底層分別調(diào)用了sidetable_retain()sidetable_release()酷誓。

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    //根據(jù)對象的地址,從一大堆散列表中态坦,獲取當前對象引用計數(shù)的散列表
    SideTable& table = SideTables()[this];
    
    //自旋鎖,加鎖
    table.lock();
    //獲取引用計數(shù)
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        //引用計數(shù)增加
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    //自旋鎖,解鎖
    table.unlock();

    return (id)this;
}
// rdar://20206767
// return uintptr_t instead of bool so that the various raw-isa 
// -release paths all return zero in eax
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    //根據(jù)對象的地址盐数,從大散列表中,獲取當前對象引用計數(shù)的散列表
    SideTable& table = SideTables()[this];
    //定義局部變量伞梯,是否需要dealloc
    bool do_dealloc = false;
    
    //自旋鎖加鎖
    table.lock();
    //獲取當前對象的引用計數(shù)
    RefcountMap::iterator it = table.refcnts.find(this);
    
    //判斷是否需要dealloc
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        //如果不需要dealloc玫氢,則引用計數(shù)減1
        it->second -= SIDE_TABLE_RC_ONE;
    }
    //自旋鎖解鎖
    table.unlock();
    
    //如果需要dealloc則發(fā)送消息,調(diào)用SEL_dealloc
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

存儲SideTable的全局哈希映射表StripedMap

static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

散列表SideTable的結(jié)構(gòu):

struct SideTable {
    spinlock_t slock;//內(nèi)核自旋鎖spinlock_t
    RefcountMap refcnts;//引用計數(shù)字典map
    weak_table_t weak_table;//weak表

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }
    
    //自旋鎖提供的加鎖解鎖方法
    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    //提供給weak操作的地址順序鎖
    // Address-ordered lock discipline for a pair of side tables.
    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

思考:為什么Runtime會映射維護多張散列表SideTable谜诫?而不是維護一張散列表?

回答:由于散列表中的操作會加鎖(比如引用計數(shù)增加漾峡、減少都會加自旋鎖),如果只有一個表的話喻旷,假如我有兩個類Person生逸,Student,那么我對Person對象進行操作的時候且预,再想對Student對象進行操作槽袄,就只能等待Person的操作完成解鎖后才能操作,這樣效率會大大下降锋谐。并且多次對同一張表進行操作遍尺,提高了使用頻率,可能會造成稍微有修改就需要動整張表的不夠合理的操作怀估。

而使用了多張表狮鸭,就避免了以上的幾種問題,哪個類需要處理就去改對應(yīng)的表多搀,而且避開了鎖的問題歧蕉,效率上也提升了很高,用空間換時間康铭。

分析源碼總結(jié)retain和release的實現(xiàn)邏輯如下:

Runtime維護了一個全局哈希映射表StripedMap惯退,根據(jù)對象可以在全局映射表中可以獲取該對象對應(yīng)的散列表SideTable,該SideTable擁有三個成員變量从藤,一個自旋鎖spinlock_t催跪,一個引用計數(shù)表RefcountMap锁蠕,以及一個weak表weak_table_t。引用計數(shù)表RefcountMap以對象的地址作為key懊蒸,以引用計數(shù)作為value荣倾。

retainobjc_object在底層調(diào)用了sidetable_retain(),查找引用計數(shù)表骑丸,做了引用計數(shù)refcntStorage+=SIDE_TABLE_RC_ONE;的操作舌仍;

releaseobjc_object在底層調(diào)用了sidetable_release(),查找引用計數(shù)表通危,做了引用計數(shù)refcntStorage-=SIDE_TABLE_RC_ONE;的操作铸豁;如果引用計數(shù)小于閥值SIDE_TABLE_DEALLOCATING,就調(diào)用SEL_dealloc

圖文總結(jié)如下:

retain&release底層實現(xiàn).png

由于SideTable結(jié)構(gòu)體中包含一個自旋鎖菊碟,筆者此處拓展一下自旋鎖相關(guān)知識:互斥鎖的作用节芥,以及和自旋鎖的區(qū)別

自旋鎖是互斥鎖的一種實現(xiàn),互斥鎖的作用就是確保同一時間只有一個線程訪問數(shù)據(jù)逆害,對資源加鎖后头镊,會等待資源解鎖,在這期間會阻塞線程忍燥,直到解鎖拧晕;而自旋鎖則是忙等隙姿,在加鎖后梅垄,會不斷地去詢問判斷是否解鎖。兩者的區(qū)別主要是:在等待期間互斥鎖會放棄CPU输玷,而自旋鎖會不斷的循環(huán)測試鎖的狀態(tài)队丝,會一直占用CPU。

總結(jié)之后欲鹏,來看一個retainCount經(jīng)典的面試題:

對象進行alloc和init之后的引用計數(shù)值為多少机久?到底是0還是1?為什么赔嚎?

帶著問題膘盖,我們源碼中全局搜索retainCount {找到實現(xiàn),并進入rootRetainCount

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

rootRetainCount源碼尤误,注意代碼邏輯

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    //散列表SideTable提供的自旋鎖侠畔,加鎖
    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    
    //判斷bits.nonpointer
    if (bits.nonpointer) {
        //這里先直接+1
        uintptr_t rc = 1 + bits.extra_rc;
        //判斷有沒有使用sideTable存儲引用計數(shù),如果有损晤,就加上
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        //散列表自旋鎖解鎖
        sidetable_unlock();
        
        //沒有就直接返回软棺,
        return rc;
    }
    
    //散列表SideTable提供的自旋鎖,解鎖
    sidetable_unlock();
    
    //直接返回sidetable_retainCount
    return sidetable_retainCount();
}

下面是sidetable_retainCount的實現(xiàn)

uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];
    //設(shè)置初始值1
    size_t refcnt_result = 1;
    
    table.lock();
    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;
}

打上斷點尤勋,對象創(chuàng)建走的是bits.nonpointer為1的邏輯喘落,此時bits.extra_rc存儲的引用計數(shù)為0茵宪,但是引用計數(shù)rc = 1 + bits.extra_rc,所以返回了1

從這個源碼中我們又看出一件事瘦棋,引用計數(shù)不一定存儲在哈希表中稀火,還有可能存儲在類isa的
bits.extra_rc中,找到isa的結(jié)構(gòu)isa_t以及isa_t的結(jié)構(gòu)中找到宏定義ISA_BITFIELD筆者把宏定義整理了一下赌朋,注意其中的
has_sidetable_rcextra_rc

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        uintptr_t nonpointer        : 1;  // 0:普通指針憾股,1:優(yōu)化過,使用位域存儲更多信息
    uintptr_t has_assoc         : 1;  // 對象是否含有或曾經(jīng)含有關(guān)聯(lián)引用
    uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析構(gòu)函數(shù)或OC的dealloc
    uintptr_t shiftcls          : 33; // 存放著 Class箕慧、Meta-Class 對象的內(nèi)存地址信息
    uintptr_t magic             : 6;  // 用于在調(diào)試時分辨對象是否未完成初始化
    uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
    uintptr_t deallocating      : 1;  // 對象是否正在釋放
    uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 來存儲引用計數(shù)
    uintptr_t extra_rc          : 19;  // 引用計數(shù)能夠用 19 個二進制位存儲時服球,直接存儲在這里
    };
#endif
};

經(jīng)過分析,isa_t里面的extra_rc也是用來存儲引用計數(shù)的颠焦,只不過大小有限斩熊,如果超過了大小,就會存儲到散列表SideTable中伐庭。

總結(jié)剛才的答案如下:

問題:對象進行alloc和init之后的引用計數(shù)值為多少粉渠?到底是0還是1?為什么圾另?

回答:對象alloc之后霸株,在引用計數(shù)表中的引用計數(shù)其實為0,只是在獲取retainCount的方法rootRetainCount的內(nèi)部進行了+1的操作

總結(jié)對象生命周期引用計數(shù)的變化圖如下:

retainCount流程.png

四集乔、內(nèi)存管理之dealloc底層實現(xiàn)


直接進入源碼搜索dealloc {去件,找到底層函數(shù)調(diào)用流程如下:

  1. dealloc底層調(diào)用_objc_rootDealloc()
  2. _objc_rootDealloc()調(diào)用objc_object::rootDealloc()
  3. objc_object::rootDealloc()調(diào)用object_dispose()
  4. object_dispose()進行了free(obj)釋放對象,同時調(diào)用objc_destructInstance()
  5. objc_destructInstance()函數(shù)中判斷是否有析構(gòu)函數(shù)和關(guān)聯(lián)引用扰路,如果有侣肄,就要移除诉濒,最后調(diào)用clearDeallocating()
  6. clearDeallocating()中進行引用計數(shù)refcnt的清除和weak指針的移除兼贡,并調(diào)用weak_clear_no_lock()(這個weak指針移除具體步驟在下面的weak指針清除的時候進行詳細分析份招。)

下面貼出上述步驟的所有源碼:

1、dealloc底層調(diào)用_objc_rootDealloc()

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

2哩罪、_objc_rootDealloc()調(diào)用objc_object::rootDealloc()

void
_objc_rootDealloc(id obj)
{
    assert(obj);
    obj->rootDealloc();
}

3授霸、objc_object::rootDealloc()調(diào)用object_dispose()

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

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

4、object_dispose()進行了對象的釋放free(obj)际插,同時調(diào)用objc_destructInstance()

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

    objc_destructInstance(obj);  
    //釋放obj  
    free(obj);

    return nil;
}

5碘耳、在objc_destructInstance()函數(shù)中判斷是否有析構(gòu)函數(shù)和關(guān)聯(lián)引用,如果有腹鹉,就要移除藏畅,最后調(diào)用clearDeallocating()

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
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);
        obj->clearDeallocating();
    }

    return obj;
}
inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    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());
}

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

總結(jié)一下dealloc主要做了下面這些內(nèi)容:

  1. 首先判斷并調(diào)用對象C++的析構(gòu)函數(shù),釋放對象占用的空間,順序是:先子類->后父類 ----- object_cxxDestruct()
  2. 然后清除對象關(guān)聯(lián)引用 ----- _object_remove_assocations()
  3. 然后從weak表中清空并移除對象對應(yīng)的所有weak指針 -----weak_clear_no_lock()
  4. 然后移除引用計數(shù) ----- table.refcnts.erase(it);
  5. 最后釋放對象 free(obj)

對應(yīng)流程圖總結(jié)如下:

dealloc流程圖.png

五愉阎、內(nèi)存管理之weak底層實現(xiàn)


分析完retain/release以及dealloc的底層實現(xiàn)绞蹦,我們來繼續(xù)分析weak的底層實現(xiàn)及weak表的插入和移除操作:

當我們開發(fā)時用__weak修飾變量,其實Runtime會在底層調(diào)用objc_initWeak函數(shù)榜旦,objc_initWeak()底層又調(diào)用了storeWeak()幽七。storeWeak()的作用就是向表中注冊弱引用指針,或者更新表溅呢,下面是storeWeak()的實現(xiàn)部分澡屡,筆者把注釋寫在里面:

/** 
 * 注意注釋部分!8谰伞驶鹉!注釋已經(jīng)說的很直白了
 * Initialize a fresh weak pointer to some object location. 
 * It would be used for code like: 
 *
 * (The nil case) 
 * __weak id weakPtr;
 * (The non-nil case) 
 * NSObject *o = ...;
 * __weak id weakPtr = o;
 * 
 * This function IS NOT thread-safe with respect to concurrent 
 * modifications to the weak variable. (Concurrent weak clear is safe.)
 *
 * @param location Address of __weak ptr. 
 * @param newObj Object ptr. 
 */
id
objc_initWeak(id *location, id newObj)
{
    
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

static id 
storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;
    //

    // Acquire locks for old and new values.
    獲取舊值和新值的鎖。
    // Order by lock address to prevent lock ordering problems. 
    按鎖地址排序铣墨,以防止出現(xiàn)鎖排序問題室埋。
    // Retry if the old value changes underneath us.
    如果舊值在之后改變就重試操作
 retry:
    //獲取舊值和新值
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    //加鎖
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }
    //
    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    //防止初始化機制和弱引用機制之間出現(xiàn)死鎖
    
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    //分配新值之前,清除舊值
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    // 分配新值
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        //沒有新值就不改
        // No new value. The storage is not changed.
    }
    
    //解鎖
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

然后我們來看一下SideTable中weak表weak_table_t的源碼:

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    //保存了所有指向指定對象的 weak 指針
    weak_entry_t *weak_entries;
    //存儲空間
    size_t    num_entries;
    //引用計數(shù)輔助量
    uintptr_t mask;
    //最大偏移值
    uintptr_t max_hash_displacement;
};

從之前引用計數(shù)的分析以及weak表的源碼中分析得出:Runtime維護了一個全局哈希映射表StripedMap伊约,不同對象映射著對應(yīng)的散列表SideTable姚淆,散列表中包含引用計數(shù)map以及weak弱引用表weak_table_tweak_table_t中保存了所有指定對象的weak指針屡律,用對象的地址作為key腌逢,weak_entry_t結(jié)構(gòu)體對象作為value;weak_entry_t負責維護和存儲指向一個對象的所有弱引用的散列表超埋。

weak_entry_t源碼:

/**
 * The internal structure stored in the weak references table. 
 * It maintains and stores
 * a hash set of weak references pointing to an object.
 * If out_of_line_ness != REFERRERS_OUT_OF_LINE then the set
 * is instead a small inline array.
 */

typedef DisguisedPtr<objc_object *> weak_referrer_t;
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    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表的存儲結(jié)構(gòu)思維導(dǎo)圖總結(jié)如下:

weak底層存儲結(jié)構(gòu).png

針對weak_table_t還提供了三個主要的方法:向表中添加元素的方法weak_register_no_lock搏讶、從表中移除元素的weak_unregister_no_lock、以及清空weak指針weak_clear_no_lock

向添加weak_table_t中添加方法weak_register_no_lock的源碼:

/** 
 * Registers a new (object, weak pointer) pair. Creates a new weak
 * object entry if it does not exist.
 * 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
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;
    //weak指針地址
    objc_object **referrer = (objc_object **)referrer_id;

    ......
    
    // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //添加到表中
        append_referrer(entry, referrer);
    } 
    else {
        //創(chuàng)建新的weak_entry_t實體
        //根據(jù)對象及弱指針生成一個weak_entry_t結(jié)構(gòu)體對象纳本,并插入表中
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        //插入到表中
        weak_entry_insert(weak_table, &new_entry);
    }

    return referent_id;
}


移除方法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;
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;

    if (!referent) return;

    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //移除表中的weak指針
        remove_referrer(entry, referrer);
        
        ......

        if (empty) {
            //移除weak_table表中對應(yīng)的的entry
            weak_entry_remove(weak_table, entry);
        }
    }
}

清空對象的weak指針窍蓝,調(diào)用時機就是對象dealloc的時候:

/** 
 * 調(diào)用時機:
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;
    //獲取weak_entry_t
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    //獲取weak_entry_t中的weak_referent_t腋颠,遍歷weak_referrer_t繁成,將其中的weak指針置為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();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

總結(jié)一下:

1、weak底層實現(xiàn)的流程

  1. Runtime全局維護了一個全局映射表StripedMap淑玫,根據(jù)對象的地址能夠獲取對應(yīng)的散列表SideTable(注意=硗蟆!絮蒿!也有可能是多個對象共用一個散列表)尊搬,散列表SideTable之中包含有weak表weak_table_tweak_table_t中根據(jù)對象的地址能夠查到該對象對應(yīng)的weak_entry_t實體土涝,weak_entry_t用來管理對象的所有的weak指針佛寿,weak指針存儲在weak_referrer_t中。

  2. 當我們在用__weak修飾對象的時候,運行時Runtime會在底層調(diào)用objc_initWeak()方法

  3. objc_initWeak()方法會調(diào)用storeWeak()冀泻;

  4. storeWeak()這個函數(shù)會先判斷對象是否初始化常侣,如果未初始化,則進行對象初始化弹渔,然后創(chuàng)建對應(yīng)的SideTable胳施;如果對象已經(jīng)有SideTable,那么判斷weak指針是否需要更新肢专,更新操作就是刪除對應(yīng)location位置的weak_entry_t對象舞肆,創(chuàng)建新的weak_entry_t,然后插入到weak表weak_table_t中博杖。

2椿胯、weak指針移除原理

1、移除時機:調(diào)用對象的dealloc方法時剃根,中間會調(diào)用clearDeallocating压状,其中會調(diào)用weak_clear_no_lock對weak指針進行移除。

2跟继、移除原理:weak_clear_no_lock底層會獲取weak表weak_table_t中的實體weak_entry_t种冬,然后拿到其中的weak_referrer_t,拿到weak_referrer_t之后舔糖,遍歷并將其中的所有weak指針置為nil娱两,最后把這個weak_entry_tweak_table_t中移除。

3金吗、weak指針本質(zhì):從源碼中可以看出weak指針的類型為是objc_object**十兢,是對象的二維指針,就是指向?qū)ο蟮刂返闹羔槨?/strong>

接下來看一個問題:

如果在dealloc中使用__weak會有什么樣的結(jié)果摇庙?

答案:會crash!

回到源碼旱物,在storeWeak函數(shù)源碼的上邊找到下面這部分代碼

// Update a weak variable.
// If HaveOld is true, the variable has an existing value 
//   that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be 
//   assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is 
//   deallocating or newObj's class does not support weak references. 
//   If CrashIfDeallocating is false, nil is stored instead.

enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
          

以及注冊weak指針的方法

/** 
 * Registers a new (object, weak pointer) pair. Creates a new weak
 * object entry if it does not exist.
 * 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    
    ......
    
    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;
        }
    }

    ......

    return referent_id;
}

結(jié)合第一部分注釋部分以及第二部分的注冊函數(shù)weak_register_no_lock進行分析:如果一個對象正在進行dealloc的時候,進行weak指針的更新操作卫袒,就會直接crash宵呛!并報錯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.

注冊時這么判斷作用就是:在對象正在釋放的過程中,或者對象已經(jīng)釋放后夕凝,是不允許使用weak來引用實例變量的宝穗。這樣就是為了防止野指針的出現(xiàn)。

-END-

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末码秉,一起剝皮案震驚了整個濱河市逮矛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌转砖,老刑警劉巖须鼎,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡晋控,警方通過查閱死者的電腦和手機挑围,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糖荒,“玉大人杉辙,你說我怎么就攤上這事〈范洌” “怎么了蜘矢?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長综看。 經(jīng)常有香客問我品腹,道長,這世上最難降的妖魔是什么红碑? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任舞吭,我火速辦了婚禮,結(jié)果婚禮上析珊,老公的妹妹穿的比我還像新娘羡鸥。我一直安慰自己,他們只是感情好忠寻,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布惧浴。 她就那樣靜靜地躺著,像睡著了一般奕剃。 火紅的嫁衣襯著肌膚如雪衷旅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天纵朋,我揣著相機與錄音柿顶,去河邊找鬼。 笑死操软,一個胖子當著我的面吹牛嘁锯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播寺鸥,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼猪钮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胆建?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肘交,失蹤者是張志新(化名)和其女友劉穎笆载,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡凉驻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年腻要,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涝登。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡雄家,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胀滚,到底是詐尸還是另有隱情趟济,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布咽笼,位于F島的核電站顷编,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏剑刑。R本人自食惡果不足惜媳纬,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望施掏。 院中可真熱鬧钮惠,春花似錦、人聲如沸七芭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抖苦。三九已至毁菱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锌历,已是汗流浹背贮庞。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留究西,地道東北人窗慎。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像卤材,于是被迫代替她去往敵國和親遮斥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354