iOS Weak底層詳解

原文鏈接weak 弱引用的實現方式weak的生命週期:具體實現方法

很少有人知道weak表其實是一個hash(哈希)表,Key是所指對象的地址,Value是weak指針的地址數組。更多人的人只是知道weak是弱引用,所引用對象的計數器不會加一,並在引用對象被釋放的時候自動被設置爲nil。通常用於解決循環引用問題。但現在單知道這些已經不足以應對面試了,好多公司會問weak的原理。weak的原理是什麼呢?下面就分析一下weak的工作原理(只是自己對這個問題好奇,學習過程中的筆記,希望對讀者也有所幫助)。

weak 實現原理的概括

Runtime維護了一個weak表,用於存儲指向某個對象的所有weak指針。weak表其實是一個hash(哈希)表,Key是所指對象的地址,Value是weak指針的地址(這個地址的值是所指對象指針的地址)數組。

weak 的實現原理可以概括一下三步:

1、初始化時:runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。
2、添加引用時:objc_initWeak函數會調用 objc_storeWeak() 函數, objc_storeWeak() 的作用是更新指針指向,創建對應的弱引用表。
3、釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取所有weak指針地址的數組,然後遍歷這個數組把其中的數據設爲nil,最後把這個entry從weak表中刪除,最後清理對象的記錄。

 

下面將開始詳細介紹每一步:

 以例子開始

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSObject *p = [[NSObject alloc] init];
        __weak NSObject *p1 = p;
    }
    return 0;
}

單步運行,發現會跳入 NSObject.mm 中的 objc_initWeak() 這個方法。在進行編譯過程前,clang 其實對 __weak 做了轉換,將聲明方式做出瞭如下調整。

NSObject objc_initWeak(&p, 對象指針);

其中的對象指針,就是代碼中的 [[NSObject alloc] init] ,而 p 是我們傳入的一個弱引用指針。而對於 objc_initWeak() 方法的實現,在 runtime 中的源碼如下:

id objc_initWeak(id *location, id newObj) {
	// 查看對象實例是否有效
	// 無效對象直接導致指針釋放
    if (!newObj) {
        *location = nil;
        return nil;
    }
    // 這裏傳遞了三個 bool 數值
    // 使用 template 進行常量參數傳遞是爲了優化性能
    return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
}

可以看出,這個函數僅僅是一個深層函數的調用入口,而一般的入口函數中,都會做一些簡單的判斷(例如 objc_msgSend 中的緩存判斷),這裏判斷了其指針指向的類對象是否有效,無效直接釋放,不再往深層調用函數。

需要注意的是,當修改弱引用的變量時,這個方法非線程安全。所以切記選擇競爭帶來的一些問題。

繼續閱讀 objc_storeWeak() 的實現:

// HaveOld:	 true - 變量有值
// 			false - 需要被及時清理,當前值可能爲 nil
// HaveNew:	 true - 需要被分配的新值,當前值可能爲 nil
// 			false - 不需要分配新值
// CrashIfDeallocating: true - 說明 newObj 已經釋放或者 newObj 不支持弱引用,該過程需要暫停
// 			false - 用 nil 替代存儲
template bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj) {
	// 該過程用來更新弱引用指針的指向
	// 初始化 previouslyInitializedClass 指針
    Class previouslyInitializedClass = nil;
    id oldObj;
    // 聲明兩個 SideTable
    // ① 新舊散列創建
    SideTable *oldTable;
    SideTable *newTable;
	// 獲得新值和舊值的鎖存位置(用地址作爲唯一標示)
	// 通過地址來建立索引標誌,防止桶重複
	// 下面指向的操作會改變舊值
  retry:
    if (HaveOld) {
    	// 更改指針,獲得以 oldObj 爲索引所存儲的值地址
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (HaveNew) {
    	// 更改新值指針,獲得以 newObj 爲索引所存儲的值地址
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
	// 加鎖操作,防止多線程中競爭衝突
    SideTable::lockTwoHaveOld, HaveNew>(oldTable, newTable);
	// 避免線程衝突重處理
	// location 應該與 oldObj 保持一致,如果不同,說明當前的 location 已經處理過 oldObj 可是又被其他線程所修改
    if (HaveOld  &&  *location != oldObj) {
        SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
        goto retry;
    }
    // 防止弱引用間死鎖
    // 並且通過 +initialize 初始化構造器保證所有弱引用的 isa 非空指向
    if (HaveNew  &&  newObj) {
    	// 獲得新對象的 isa 指針
        Class cls = newObj->getIsa();
        // 判斷 isa 非空且已經初始化
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) {
        	// 解鎖
            SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
            // 對其 isa 指針進行初始化
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
            // 如果該類已經完成執行 +initialize 方法是最理想情況
            // 如果該類 +initialize 在線程中 
            // 例如 +initialize 正在調用 storeWeak 方法
            // 需要手動對其增加保護策略,並設置 previouslyInitializedClass 指針進行標記
            previouslyInitializedClass = cls;
			// 重新嘗試
            goto retry;
        }
    }
    // ② 清除舊值
    if (HaveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
    // ③ 分配新值
    if (HaveNew) {
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, 
                                                      (id)newObj, location, 
                                                      CrashIfDeallocating);
        // 如果弱引用被釋放 weak_register_no_lock 方法返回 nil 
        // 在引用計數表中設置若引用標記位
        if (newObj  &&  !newObj->isTaggedPointer()) {
        	// 弱引用位初始化操作
			// 引用計數那張散列表的weak引用對象的引用計數中標識爲weak引用
            newObj->setWeaklyReferenced_nolock();
        }
        // 之前不要設置 location 對象,這裏需要更改指針指向
        *location = (id)newObj;
    }
    else {
        // 沒有新值,則無需更改
    }
    SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
    return (id)newObj;
}

其中標註的一些要點,開始逐一介紹:

引用計數和弱引用依賴表 SideTable

SideTable 這個結構體,我給他起名引用計數和弱引用依賴表,因爲它主要用於管理對象的引用計數和 weak 表。在 NSObject.mm 中聲明其數據結構:

struct SideTable {
	// 保證原子操作的自旋鎖
    spinlock_t slock;
    // 引用計數的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

在之前的 runtime 版本中,有一個較爲重要的成員方法,用來根據對象的地址在緩存中取出對應的 SideTable 實例:

static SideTable *tableForPointer(const void *p);

而在上面 objc_storeWeak 方法中,取出實例的方法變成了 &SideTables()[xxxObj]; 這種方式。查看方法的實現,發現瞭如下函數:

static StripedMapSideTable>& SideTables() {
    return *reinterpret_castStripedMapSideTable>*>(SideTableBuf);
}

在取出實例方法的實現中,使用了 C++ 標準轉換運算符 reinterpret_cast ,其表達方式爲:

reinterpret_cast new_type> (expression)

用來處理無關類型之間的轉換。該關鍵字會產生一個新值,並保證與原參數(expression)擁有完全相同的比特位

而 StripedMap 是一個模板類(Template Class),通過傳入類(結構體)參數,會動態修改在該類中的一個 array 成員存儲的元素類型,並且其中提供了一個針對於地址的 hash 算法,用作存儲 key。可以說, StripedMap 提供了一套擁有將地址作爲 key 的 hash table 解決方案,而該方案採用了模板類,是擁有泛型性的。

介紹了與對象相關聯的 SideTable 檢索方式,再來看 SideTable 的成員和作用。

對於 slock 和 refcnts 兩個成員不用多說,第一個是爲了防止競爭選擇的自旋鎖,第二個是協助對象的 isa 指針的 extra_rc 共同引用計數的變量(對於對象結果,在今後的文中提到)。這裏主要看 weak 全局 hash 表的結構與作用。

struct weak_table_t {
	// 保存了所有指向指定對象的 weak 指針
    weak_entry_t *weak_entries;
    // 存儲空間
    size_t    num_entries;
    // 參與判斷引用計數輔助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

這是一個全局弱引用表。使用不定類型對象的地址作爲 key ,用 weak_entry_t 類型結構體對象作爲 value 。其中的 weak_entries 成員,從字面意思上看,即爲弱引用表入口。其實現也是這樣的。

typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
    DisguisedPtrobjc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line : 1;
            uintptr_t        num_refs : PTR_MINUS_1;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line=0 is LSB of one of these (don't care which)
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
 }

在 weak_entry_t 的結構中,DisguisedPtr referent 是對泛型對象的指針做了一個封裝,通過這個泛型類來解決內存泄漏的問題。從註釋中寫 out_of_line 成員爲最低有效位,當其爲0的時候, weak_referrer_t 成員將擴展爲多行靜態 hash table。其實其中的 weak_referrer_t 是二維 objc_object 的別名,通過一個二維指針地址偏移,用下標作爲 hash 的 key,做成了一個弱引用散列。

那麼在有效位未生效的時候,out_of_line 、 num_refs、 mask 、 max_hash_displacement 有什麼作用?以下是筆者自身的猜測:

  • out_of_line:最低有效位,也是標誌位。當標誌位 0 時,增加引用表指針緯度。
  • num_refs:引用數值。這裏記錄弱引用表中引用有效數字,因爲弱引用表使用的是靜態 hash 結構,所以需要使用變量來記錄數目。
  • mask:計數輔助量。
  • max_hash_displacement:hash 元素上限閥值。

其實 out_of_line 的值通常情況下是等於零的,所以弱引用表總是一個 objc_objective 指針二維數組。一維 objc_objective 指針可構成一張弱引用散列表,通過第三緯度實現了多張散列表,並且表數量爲 WEAK_INLINE_COUNT 。

總結一下 StripedMap[] : StripedMap 是一個模板類,在這個類中有一個 array 成員,用來存儲 PaddedT 對象,並且其中對於 [] 符的重載定義中,會返回這個 PaddedT 的 value 成員,這個 value 就是我們傳入的 T 泛型成員,也就是 SideTable 對象。在 array 的下標中,這裏使用了 indexForPointer 方法通過位運算計算下標,實現了靜態的 Hash Table。而在 weak_table 中,其成員 weak_entry 會將傳入對象的地址加以封裝起來,並且其中也有訪問全局弱引用表的入口。

舊對象解除註冊操作 weak_unregister_no_lock

#define WEAK_INLINE_COUNT 4
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id) {
    // 在入口方法中,傳入了 weak_table 弱引用表,referent_id 舊對象以及 referent_id 舊對象對應的地址
    // 用指針去訪問 oldObj 和 *location  
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    weak_entry_t *entry;
	// 如果其對象爲 nil,無需取消註冊
    if (!referent) return;
	// weak_entry_for_referent 根據首對象查找 weak_entry
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
    	// 通過地址來解除引用關聯	
        remove_referrer(entry, referrer);
        bool empty = true;
        // 檢測 out_of_line 位的情況
        // 檢測 num_refs 位的情況
        if (entry->out_of_line  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
        	// 將引用表中記錄爲空
            for (size_t i = 0; i  WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }
	// 從弱引用的 zone 表中刪除
        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }
    // 這裏不會設置 *referrer = nil,因爲 objc_storeWeak() 函數會需要該指針
}

該方法主要作用是將舊對象在 weak_table 中接觸 weak 指針的對應綁定。根據函數名,稱之爲解除註冊操作。從源碼中,可以知道其功能就是從 weak_table 中接觸 weak 指針的綁定。而其中的遍歷查詢,就是針對於 weak_entry 中的多張弱引用散列表。

新對象添加註冊操作 weak_register_no_lock

id weak_register_no_lock(weak_table_t *weak_table, id referent_id,
                      id *referrer_id, bool crashIfDeallocating) {
	// 在入口方法中,傳入了 weak_table 弱引用表,referent_id 舊對象以及 referent_id 舊對象對應的地址
    // 用指針去訪問 oldObj 和 *location
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
	// 檢測對象是否生效、以及是否使用了 tagged pointer 技術
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;
    // 保證引用對象是否有效
    // hasCustomRR 方法檢查類(包括其父類)中是否含有默認的方法
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
    	// 檢查 dealloc 狀態
        deallocating = referent->rootIsDeallocating();
    }
    else {
    	// 會返回 referent 的 SEL_allowsWeakReference 方法的地址
        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);
    }
	// 由於 dealloc 導致 crash ,並輸出日誌
    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
    weak_entry_t *entry;
    // 對於給定的弱引用查詢 weak_table
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
    	// 增加弱引用表於附加對象上
        append_referrer(entry, referrer);
    } 
    else {
    	// 自行創建弱引用表
        weak_entry_t new_entry;
        new_entry.referent = referent;
        new_entry.out_of_line = 0;
        new_entry.inline_referrers[0] = referrer;
        for (size_t i = 1; i  WEAK_INLINE_COUNT; i++) {
            new_entry.inline_referrers[i] = nil;
        }
        // 如果給定的弱引用表滿容,進行自增長
        weak_grow_maybe(weak_table);
        // 向對象添加弱引用表關聯,不進行檢查直接修改指針指向
        weak_entry_insert(weak_table, &new_entry);
    }
    // 這裏不會設置 *referrer = nil,因爲 objc_storeWeak() 函數會需要該指針
    return referent_id;
}

這一步與上一步相反,通過 weak_register_no_lock 函數把心的對象進行註冊操作,完成與對應的弱引用表進行綁定操作。

初始化弱引用對象流程一覽

弱引用的初始化,從上文的分析中可以看出,主要的操作部分就在弱引用表的取鍵、查詢散列、創建弱引用表等操作,可以總結出如下的流程圖:

這個圖中省略了很多情況的判斷,但是當聲明一個 __weak 會調用上圖中的這些方法。當然, storeWeak 方法不僅僅用在 __weak 的聲明中,在 class 內部的操作中也會常常通過該方法來對 weak 對象進行操作。

而當weak引用指向的對象被釋放時,又是如何去處理weak指針的呢?當釋放對象時,其基本流程如下:

  1. 調用objc_release

  2. 因爲對象的引用計數爲0,所以執行dealloc

  3. 在dealloc中,調用了_objc_rootDealloc函數

  4. 在_objc_rootDealloc中,調用了object_dispose函數

  5. 調用objc_destructInstance

  6. 最後調用objc_clear_deallocating

我們重點關注一下最後一步,objc_clear_deallocating的具體實現如下:

void objc_clear_deallocating(id obj) 
{
    ......
    SideTable *table = SideTable::tableForPointer(obj);
    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    OSSpinLockLock(&table->slock);
    if (seen_weak_refs) {
        arr_clear_deallocating(&table->weak_table, obj);
    }
    ......
}

我們可以看到,在這個函數中,首先取出對象對應的SideTable實例,如果這個對象有關聯的弱引用,則調用arr_clear_deallocating來清除對象的弱引用信息。我們來看看arr_clear_deallocating具體實現:

PRIVATE_EXTERN void arr_clear_deallocating(weak_table_t *weak_table, id referent) {
    {
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
        if (entry == NULL) {
            ......
            return;
        }
        // zero out references
        for (int i = 0; i < entry->referrers.num_allocated; ++i) {
            id *referrer = entry->referrers.refs[i].referrer;
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
                else if (*referrer) {
                    _objc_inform("__weak variable @ %p holds %p instead of %p\n", referrer, *referrer, referent);
                }
            }
        }
        weak_entry_remove_no_lock(weak_table, entry);
        weak_table->num_weak_refs--;
    }
}

這個函數首先是找出對象對應的weak_entry_t鏈表,然後挨個將弱引用置爲nil。最後清理對象的記錄。

objc_clear_deallocating該函數的動作如下:

1、從weak表中獲取廢棄對象的地址爲鍵值的記錄
2、將包含在記錄中的所有附有 weak修飾符變量的地址,賦值爲nil
3、將weak表中該記錄刪除
4、從引用計數表中刪除廢棄對象的地址爲鍵值的記錄

看了objc-weak.mm的源碼就明白了:其實Weak表是一個hash(哈希)表,然後裏面的key是指向對象的地址,Value是Weak指針的地址的數組。

通過上面的描述,我們基本能瞭解一個weak引用從生到死的過程。從這個流程可以看出,一個weak引用的處理涉及各種查表、添加與刪除操作,還是有一定消耗的。所以如果大量使用__weak變量的話,會對性能造成一定的影響。那麼,我們應該在什麼時候去使用weak呢?《Objective-C高級編程》給我們的建議是隻在避免循環引用的時候使用__weak修飾符。

另外,在clang中,還提供了不少關於weak引用的處理函數。如objc_loadWeak, objc_destroyWeak, objc_moveWeak等,我們可以在蘋果的開源代碼中找到相關的實現。等有時間,我再好好研究研究

補充:.m和.mm的區別

.m:源代碼文件,這個典型的源代碼文件擴展名,可以包含OC和C代碼。
.mm:源代碼文件,帶有這種擴展名的源代碼文件,除了可以包含OC和C代碼之外,還可以包含C++代碼。僅在你的OC代碼中確實需要使用C++類或者特性的時候才用這種擴展名。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章