iOS 底層解析weak的實現(xiàn)原理

本文系轉(zhuǎn)載,原文地址

對于 runtime 的分析還有很長的路,最近在寫 block 系列的同時,也回顧一下之前疏漏的細節(jié)知識。這篇文章是關于 weak 的具體實現(xiàn)的學習筆記。

runtime 對 __weak 弱引用處理方式

切入主題,這里筆者使用的 runtime 版本為 objc4-680.tar.gz。 我在入口文件 main.m 中加入如下代碼:

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

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

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

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

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

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

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

繼續(xù)閱讀 objc_storeWeak() 的實現(xiàn):

// HaveOld:  true - 變量有值
//          false - 需要被及時清理,當前值可能為 nil
// HaveNew:  true - 需要被分配的新值,當前值可能為 nil
//          false - 不需要分配新值
// CrashIfDeallocating: true - 說明 newObj 已經(jīng)釋放或者 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
    // ① 新舊散列創(chuàng)建
    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::lockTwo<HaveOld, HaveNew>(oldTable, newTable);
    // 避免線程沖突重處理
    // location 應該與 oldObj 保持一致,如果不同,說明當前的 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 方法
            // 需要手動對其增加保護策略,并設置 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 
        // 在引用計數(shù)表中設置若引用標記位
        if (newObj  &&  !newObj->isTaggedPointer()) {
            // 弱引用位初始化操作
            // 引用計數(shù)那張散列表的weak引用對象的引用計數(shù)中標識為weak引用
            newObj->setWeaklyReferenced_nolock();
        }
        // 之前不要設置 location 對象,這里需要更改指針指向
        *location = (id)newObj;
    }
    else {
        // 沒有新值,則無需更改
    }
    SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
    return (id)newObj;
}

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

引用計數(shù)和弱引用依賴表 SideTable

SideTable 這個結(jié)構(gòu)體,我給他起名引用計數(shù)和弱引用依賴表,因為它主要用于管理對象的引用計數(shù)和 weak 表。在 NSObject.mm 中聲明其數(shù)據(jù)結(jié)構(gòu):

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

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

static SideTable *tableForPointer(const void *p);

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

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

在取出實例方法的實現(xiàn)中,使用了 C++ 標準轉(zhuǎn)換運算符 reinterpret_cast ,其表達方式為:

reinterpret_cast <new_type> (expression)

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

StripedMap 是一個模板類(Template Class),通過傳入類(結(jié)構(gòu)體)參數(shù),會動態(tài)修改在該類中的一個 array 成員存儲的元素類型,并且其中提供了一個針對于地址的 hash 算法,用作存儲 key。可以說, StripedMap 提供了一套擁有將地址作為 key 的 hash table 解決方案,而該方案采用了模板類,是擁有泛型性的。

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

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

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

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

typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
    DisguisedPtr<objc_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 的結(jié)構(gòu)中,DisguisedPtr<objc_object> referent 是對泛型對象的指針做了一個封裝,通過這個泛型類來解決內(nèi)存泄漏的問題。從注釋中寫 out_of_line 成員為最低有效位,當其為0的時候, weak_referrer_t 成員將擴展為多行靜態(tài) hash table。其實其中的 weak_referrer_t 是二維 objc_object 的別名,通過一個二維指針地址偏移,用下標作為 hash 的 key,做成了一個弱引用散列。

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

  • out_of_line:最低有效位,也是標志位。當標志位 0 時,增加引用表指針緯度。
  • num_refs:引用數(shù)值。這里記錄弱引用表中引用有效數(shù)字,因為弱引用表使用的是靜態(tài) hash 結(jié)構(gòu),所以需要使用變量來記錄數(shù)目。
  • mask:計數(shù)輔助量。
  • max_hash_displacement:hash 元素上限閥值。

其實 out_of_line 的值通常情況下是等于零的,所以弱引用表總是一個 objc_objective 指針二維數(shù)組。一維 objc_objective 指針可構(gòu)成一張弱引用散列表,通過第三緯度實現(xiàn)了多張散列表,并且表數(shù)量為 WEAK_INLINE_COUNT 。

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

sidetable.png

舊對象解除注冊操作 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 根據(jù)首對象查找 weak_entry
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        // 通過地址來解除引用關聯(lián)  
        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() 函數(shù)會需要該指針
}

該方法主要作用是將舊對象在 weak_table 中接觸 weak 指針的對應綁定。根據(jù)函數(shù)名,稱之為解除注冊操作。從源碼中,可以知道其功能就是從 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 狀態(tài)
        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 {
        // 自行創(chuàng)建弱引用表
        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);
        // 向?qū)ο筇砑尤跻帽黻P聯(lián),不進行檢查直接修改指針指向
        weak_entry_insert(weak_table, &new_entry);
    }
    // 這里不會設置 *referrer = nil,因為 objc_storeWeak() 函數(shù)會需要該指針
    return referent_id;
}

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

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

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

weaktable-2

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

以上就是對于 weak 弱引用對象的初始化時 runtime 內(nèi)部的執(zhí)行過程,想必閱讀后會對其結(jié)構(gòu)有更深的理解。

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

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

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