iOS weak實(shí)現(xiàn)原理和銷毀過(guò)程

前言

weak弱引用的相關(guān)內(nèi)容在開(kāi)發(fā)中常遇到,那么這篇文章我們主要探索weak的底層操作是什么樣子的,開(kāi)始吧!

準(zhǔn)備工作

1. weak基本用法

weak是弱引用,用weak來(lái)修飾、描述所引用對(duì)象的計(jì)數(shù)器并不會(huì)增加,而且weak會(huì)在引用對(duì)象被釋放的時(shí)候自動(dòng)置為nil,這也就避免了野指針訪問(wèn)壞內(nèi)存而引起奔潰的情況,另外weak也可以解決循環(huán)引用。

面試拓展:為什么修飾代理使用weak而不是用assign
assign可用來(lái)修飾基本數(shù)據(jù)類型,也可修飾OC的對(duì)象,但如果用 assign修飾對(duì)象類型指向的是一個(gè)強(qiáng)指針,當(dāng)指向的這個(gè)指針釋放之后,它仍指向這塊內(nèi)存,必須要手動(dòng)給置為nil,否則會(huì)產(chǎn)生野指針,如果還通過(guò)此指針操作那塊內(nèi)存,會(huì)導(dǎo)致EXC_BAD_ACCESS錯(cuò)誤,調(diào)用了已經(jīng)被釋放的內(nèi)存空間;而weak只能用來(lái)修飾OC對(duì)象,而且相比assign比較安全,如果指向的對(duì)象消失了,那么它會(huì)自動(dòng)置為nil,不會(huì)導(dǎo)致野指針。

2. weak的實(shí)現(xiàn)原理

Runtime維護(hù)了一個(gè)weak表,用于存儲(chǔ)指向某個(gè)對(duì)象的所有weak指針。weak表其實(shí)是一個(gè)hash哈希)表,Key是所指對(duì)象的地址,Valueweak指針的地址 (這個(gè)地址的值是所指對(duì)象的地址)數(shù)組。

weak的實(shí)現(xiàn)原理可以概括一下三步:

  • 初始化時(shí)runtime會(huì)調(diào)用objc_initWeak函數(shù),初始化一個(gè)新的weak指針指向對(duì)象的地址。
  • 添加引用時(shí)objc_initWeak函數(shù)會(huì)調(diào)用objc_storeWeak()函數(shù),objc_storeWeak()的作用是更新指針指向,創(chuàng)建對(duì)應(yīng)的弱引用表。
  • 釋放時(shí):調(diào)用clearDeallocating函數(shù)。clearDeallocating函數(shù)首先根據(jù)對(duì)象地址獲取所有weak指針地址的數(shù)組,然后遍歷這個(gè)數(shù)組把其中的數(shù)據(jù)設(shè)為nil,最后把這個(gè)entryweak表中刪除,最后清理對(duì)象的記錄。

3. weak底層原理探索

以下案例就是使用一個(gè)臨時(shí)變量obc1來(lái)存儲(chǔ)創(chuàng)建出來(lái)的變量,代碼如下:

    NSObject * objc1 = [[NSObject alloc] init];
    id  __weak objc2 = objc1;

注意:
一般情況下,我們不會(huì)直接用__weak來(lái)修飾一個(gè)剛創(chuàng)建出來(lái)的臨時(shí)變量,因?yàn)?code>__weak修飾這個(gè)變量,運(yùn)行時(shí)一創(chuàng)建出來(lái)就會(huì)釋放,而如果使用一個(gè)臨時(shí)變量objc1話,創(chuàng)建出來(lái)后會(huì)放到自動(dòng)釋放池中,延緩這個(gè)變量的生命周期,變量的生命周期會(huì)跟著自動(dòng)釋放池自動(dòng)保持,所以這樣能夠保證不會(huì)一創(chuàng)建出來(lái)就會(huì)釋放。

然后我們運(yùn)行代碼,打開(kāi)匯編斷點(diǎn)查看底層調(diào)用了什么方法,如下:

打開(kāi)匯編斷點(diǎn)

進(jìn)入斷點(diǎn)查看,如下:
進(jìn)入?yún)R編斷點(diǎn)

通過(guò)匯編斷點(diǎn)可以看出底層是調(diào)用了objc_initWeak方法,這是入口。

4. 初始化源碼分析

進(jìn)入objc_initWeak的內(nèi)部添加斷點(diǎn),進(jìn)行lldb調(diào)試。

  • objc_initWeak分析
    libobjc.dylib源碼中成功定位到了初始化過(guò)程,同時(shí)objc_initWeak傳入了兩個(gè)參數(shù),location即弱引用的地址存儲(chǔ)在棧中),newObjc也就是創(chuàng)建的對(duì)象(存儲(chǔ)在堆中)。見(jiàn)下圖:
    objc_initWeak內(nèi)部斷點(diǎn)

    根據(jù)以上的源碼,可以分析得出:
  • 首先會(huì)判斷對(duì)象是否為空,如果為空直接返回nil
  • 如果不為空,則會(huì)調(diào)用stroeWeak方法進(jìn)行存儲(chǔ)。
  • location弱引用的地址
  • newObjc是一個(gè)對(duì)象,在底層對(duì)象的實(shí)現(xiàn)就是objc_object。

注意:object必須是一個(gè)沒(méi)有被注冊(cè)為_(kāi)_weak對(duì)象的有效指針。

4.1數(shù)據(jù)結(jié)構(gòu)分析

在分析方法之前進(jìn)行數(shù)據(jù)結(jié)構(gòu)的分析是非常必要的!系統(tǒng)維護(hù)了一個(gè)SideTables,那么SideTable散列表為什么有多張?一張表不安全太多了性能不好。真機(jī)下8張表,其他環(huán)境下64張,散列表也是一張hash表。而SideTables是一個(gè)hash表,綜合了鏈表和數(shù)組的特點(diǎn)。拉鏈法,同一個(gè)hash可以放在同一個(gè)表中。

1. 散列表SideTable

struct SideTable {
    //自旋鎖
    spinlock_t slock;
    //引用計(jì)數(shù)
    RefcountMap refcnts;
     //對(duì)象的弱引用表
    weak_table_t weak_table;
    .....
    //后面還有一些對(duì)鎖的操作
}

SideTable是個(gè)結(jié)構(gòu)體,其屬性包括:自旋鎖引用計(jì)數(shù)表弱引用依賴表。
2. 弱引用表weak_table

struct weak_table_t {
    //保存了所有指向指定對(duì)象的 weak 指針
    weak_entry_t *weak_entries;
    //存儲(chǔ)空間
    size_t    num_entries;
    //哈希算法輔助值(掩碼)
    uintptr_t mask;
    //最大偏移量
    uintptr_t max_hash_displacement;
};

weak表是一個(gè)弱引用表,實(shí)現(xiàn)為一個(gè)weak_table_t結(jié)構(gòu)體,存儲(chǔ)了某個(gè)對(duì)象相關(guān)的所有的弱引用信息,這是一個(gè)hash表。使用不定類型對(duì)象的地址作為key,用weak_entry_t類型結(jié)構(gòu)體對(duì)象作為value。其中的weak_entries成員,即為弱引用表入口。

3. weak_entry_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 {
            //最開(kāi)始會(huì)存放到inline_referrers, 放滿之后存放到上面的 referrers
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
    ......
    //還有一些結(jié)構(gòu)體提供的方法
}

弱引用實(shí)體,有兩個(gè)屬性,一個(gè)對(duì)象,另一個(gè)屬性是一個(gè)聯(lián)合體,其中包含弱引用的數(shù)組

數(shù)據(jù)關(guān)系圖:

關(guān)系圖

4.2 objc_storeWeak分析

結(jié)束完所需類型的數(shù)據(jù)結(jié)構(gòu)分析后,我們就開(kāi)始進(jìn)入核心方法的流程分析了,即分析objc_storeWeak方法,該方法主要分為三個(gè)功能,如下:

    1. 判斷應(yīng)用是否存在值


      功能1
    1. 如果有舊值,將舊值清除


      功能2
  • 有新增,重新初始化
    功能3

    根據(jù)上面的源碼可以知道,objc_storeWeak主要做了以下的三點(diǎn):
  • 對(duì)當(dāng)前引用的數(shù)據(jù)處理判斷,判斷該引用是否存在舊值以及是否指向了新值
  • 如果引用當(dāng)前有指向的值,即存在舊值,則需要將舊值清除;
  • 同時(shí)如果引用指向了新的對(duì)象,即存在新值,則需要進(jìn)行對(duì)應(yīng)對(duì)象弱引用的初始化工作。

我們從弱引用指向一個(gè)新值開(kāi)始探索,整理其核心代碼如下:

 if (haveNew) {
       //根據(jù)對(duì)象回去對(duì)應(yīng)的散列表
        newTable = &SideTables()[newObj];
    }
 if (haveNew) {
       // 從散列表中獲取弱引用表,并傳入
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

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

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }

進(jìn)入weak_register_no_lock流程。代碼如下:

//對(duì)象對(duì)應(yīng)的全局弱引用表,對(duì)象指針,弱引用指針
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
    //對(duì)象
    objc_object *referent = (objc_object *)referent_id;
    //弱引用指針
    objc_object **referrer = (objc_object **)referrer_id;

  ......

    // now remember it and where it is being stored
    weak_entry_t *entry;
    //根據(jù)弱引用對(duì)象從weak_table中找出weak_entry_t
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //將弱引用指針加入entry
        append_referrer(entry, referrer);
    } 
    else {
        //通過(guò)弱引用指針與對(duì)象創(chuàng)建new_entry
        weak_entry_t new_entry(referent, referrer);
        //weak_table擴(kuò)容
        weak_grow_maybe(weak_table);
        //將new_entry插入weak_table
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.
    //返回對(duì)象
    return referent_id;
}
  • weak_table中找到對(duì)象對(duì)應(yīng)的weak_entry_t
  • 找到對(duì)應(yīng)的weak_entry_t調(diào)用append_referrer將弱引用指針加入weak_entry_t(因?yàn)獒尫胚^(guò)程中有置空操作,所以找空位nil加入,這個(gè)過(guò)程中可能會(huì)進(jìn)行擴(kuò)容)。
  • 找不到則根據(jù)對(duì)象和弱引用指針創(chuàng)建weak_entry_t。
    • 調(diào)用weak_grow_maybe嘗試擴(kuò)容。
    • 調(diào)用weak_entry_insert將創(chuàng)建的weak_entry_t加入weak_table。

方法所傳入的前三個(gè)參數(shù)分別為:弱引用表、對(duì)象、弱引用。該方法會(huì)判斷對(duì)象是否析構(gòu),如果有就不會(huì)處理,直接返回。如果沒(méi)有,則會(huì)通過(guò)weak_entry_for_referent方法獲取對(duì)象對(duì)應(yīng)的weak_entry_t見(jiàn)下圖:

weak_entry_for_referent

通過(guò)hash函數(shù)獲取其表中的下標(biāo),通過(guò)循環(huán)弱引用表,找對(duì)應(yīng)的下標(biāo),獲取對(duì)應(yīng)的weak_entry_t。獲取weak_entry_t后,去存儲(chǔ)新的弱引用對(duì)象,回到方法weak_register_no_lock,在調(diào)用weak_entry_for_referent之后,會(huì)通過(guò)append_referrer(entry, referrer);方法進(jìn)行弱引用的存儲(chǔ)。見(jiàn)下圖:
append_referrer

總結(jié):
提供維護(hù)了一個(gè)SideTables,其中多張散列表SideTable,每一張SideTable表中有自旋鎖、引用計(jì)數(shù)表、弱引用表weak_table_t。弱引用表中存儲(chǔ)一些實(shí)體weak_entry_t,實(shí)體中包括了對(duì)象弱引用數(shù)組。

如果弱引用存在舊值,具體操作是什么樣子的呢?以下是核心代碼:

    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

進(jìn)入weak_unregister_no_lock方法,如下:

weak_unregister_no_lock

在此過(guò)程中,會(huì)獲取弱應(yīng)用對(duì)象對(duì)應(yīng)的實(shí)體weak_entry,并調(diào)用remove_referrer方法,從實(shí)體中移除對(duì)應(yīng)的弱引用。見(jiàn)下圖:
remove_referrer

此過(guò)程會(huì)從對(duì)象的引用列表移除該的引用,并將弱引用個(gè)數(shù)減1。

5. 弱引用的清除流程

當(dāng)一個(gè)對(duì)象調(diào)用dealloc方法時(shí),其弱引用處理流程見(jiàn)下面代碼:

        - (void)dealloc {
            _objc_rootDealloc(self);
        }

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

        inline void objc_object::rootDealloc()
        {
            if (isTaggedPointer()) return;  // fixme necessary?
            if (fastpath(isa.nonpointer                     &&
                         !isa.weakly_referenced             &&
                         !isa.has_assoc                     &&
        #if ISA_HAS_CXX_DTOR_BIT
                         !isa.has_cxx_dtor                  &&
        #else
                         !isa.getClass(false)->hasCxxDtor() &&
        #endif
                         !isa.has_sidetable_rc))
            {
                assert(!sidetable_present());
                free(this);
            } 
            else {
                object_dispose((id)this);
            }
        }

        id object_dispose(id obj)
        {
            if (!obj) return nil;
            objc_destructInstance(obj);    
            free(obj);
            return 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, /*deallocating*/true);
                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());
        }

最終會(huì)調(diào)用clearDeallocating方法對(duì)弱引用進(jìn)行處理。針對(duì)有弱引用的對(duì)象,會(huì)調(diào)用clearDeallocating_slow();方法,最終的弱引用清除的處理流程在weak_clear_no_lock中。見(jiàn)下圖:

weak_clear_no_lock

在此過(guò)程中,首先在弱引用表中獲取對(duì)象對(duì)應(yīng)的實(shí)體,開(kāi)啟循環(huán)將數(shù)組中的弱引用全部設(shè)為nil,最后將實(shí)體從弱引用表中移除

補(bǔ)充:弱引用存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)圖

弱引用存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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