淺析weak指標的實現

lyuf發表於2019-03-18

oc中weak指標主要用於打破迴圈或者防止迴圈引用的發生,應用場景還是很廣泛的。那麼被weak修飾的指標與被指向的物件在底層的運作機制究竟怎樣的呢?為什麼在物件釋放銷燬時weak指標能自動置為nil,從而避免了野指標的錯誤?

weak指標實現原理

當物件被一個weak指標引用時,底層的實現原理就是:不對被引用的物件進行retain,而是利用雜湊表對weak指標與被指向的物件進行標記、關聯。當物件銷燬釋放記憶體管時通過之前的標記對weak指標地址進行查詢,最後把weak指標的指向置為nil

weak指標實現原始碼分析

首先weak實現的函式呼叫順序如圖

淺析weak指標的實現
完成weak指標標記需要處理的物件順序如下圖所示:

淺析weak指標的實現

關於SideTable以及根據物件指標查詢對應SideTable的分析在之前分析物件引用計數的文章中有相關的說明,這裡就不再重複了。這裡主要分析下處理weak_entry_t結構體的函式

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
     // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) { //if 判斷 是否把weak指標指向的物件是否已經有對應的entry,有的話代表物件以前有被弱引用指向
        append_referrer(entry, referrer); //把 referrer 存進 對應的 entry
    } 
    else { //物件第一次被弱指標引用
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table); //weak_table.weak_entries 擴容處理
        weak_entry_insert(weak_table, &new_entry); //吧 weak_entry_t 放入  weak_table.weak_entries中陣列,通過referent的雜湊運算&mask得到索引
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}
複製程式碼

這裡的邏輯主要是通過判斷查詢物件對應SideTableweak_table屬性中是否包含有對應的weak_entry_t結構體,查詢的實現主要是通過把物件指標經過一定的雜湊演算法運算後與上一個特定的數值(該數值通常是要查詢順序表的最大索引)後得出的索引,再根據索引查詢到特定的值與要查詢的值對比得出結果。具體邏輯可以從原始碼中看weak_entry_for_referent這個函式的實現。

物件第一次被弱引用指向處理

上面的else語句就是處理物件第一次被弱引用指向的處理。通過生成一個weak_entry_t結構體後,把其插入到weak_table中,插入的邏輯與上面判斷是否含有特定weak_entry_t的邏輯都是一樣的,通過雜湊演算法獲得所以,然後插入相應的索引位置,這裡還有一個對weak_table進行擴容的處理weak_grow_maybe(weak_table);主要是判斷weak_table的存放物件容量大於或等於總容量的3/4時對weak_table的進行兩倍容量的擴容後,再把舊資料複製到擴容後的記憶體中。

物件之前已經被弱引用指向過

這種情況就相對複雜一點,首先我們看下最終存放weak指標地址的結構體定義

struct weak_entry_t {
    //被引用的物件指標(經過包裝處理)
    DisguisedPtr<objc_object> referent; 
    
    //聯合體,用於存放weak指標地址
    union {
        struct {
            weak_referrer_t *referrers; //8位元組 當inline_referrers不夠存放資料的時候,使用該指標在堆上開闢空間存放資料
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;//8位元組
            uintptr_t        mask;//8位元組
            uintptr_t        max_hash_displacement;//8位元組
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT]; // 32位元組
        };
    };
}
複製程式碼

weak_entry_t結構體主要是通過上面的聯合體(union)來存放物件的weak指標地址。程式碼中的聯合體分為兩部分,記憶體大小為64個位元組,在新增weak指標的時候優先使用的是inline_referrers陣列存放,如果陣列的已經存滿了資料(4個指標),就會用上面的結構體的陣列指標來在堆上開闢空間來存放資料,這樣就可以存放較多的weak指標地址。

weak指陣自動置nil原理

如果物件有被weak指標指向的話,在物件銷燬釋放記憶體執行-dealloc方法的時候,根據函式的呼叫順序會執行到weak_clear_no_lock這個方法。我們來看下次方法是怎樣使weakPointer = nil

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);

    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;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

複製程式碼

可以看出上面程式碼主要是獲取到對應物件的weak_entry_t *entry,根據其內部結構體獲取到存放的weak指標地址的個數,然後遍歷存放的weak指標的記憶體,把weak指標置為nil

相關文章