iOS 底層解析weak的實現原理

本文系轉載,原文地址

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

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

單步運行,發現會跳入 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 storeWeak<false/*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::lockTwo<HaveOld, HaveNew>(oldTable, newTable);
    // 避免線程沖突重處理
    // location 應該與 oldObj 保持一致,如果不同,說明當前的 location 已經處理過 oldObj 可是又被其他線程所修改
    if (HaveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
        goto retry;
    }
    // 防止弱引用間死鎖
    // 并且通過 +initialize 初始化構造器保證所有弱引用的 isa 非空指向
    if (HaveNew  &&  newObj) {
        // 獲得新對象的 isa 指針
        Class cls = newObj->getIsa();
        // 判斷 isa 非空且已經初始化
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) {
            // 解鎖
            SideTable::unlockTwo<HaveOld, 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::unlockTwo<HaveOld, 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 StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(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 {
    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 的結構中,DisguisedPtr<objc_object> referent 是對泛型對象的指針做了一個封裝,通過這個泛型類來解決內存泄漏的問題。從注釋中寫 out_of_line 成員為最低有效位,當其為0的時候, weak_referrer_t 成員將擴展為多行靜態 hash table。其實其中的 weak_referrer_t 是二維 objc_object 的別名,通過一個二維指針地址偏移,用下標作為 hash 的 key,做成了一個弱引用散列。

那么在有效位未生效的時候,out_of_linenum_refsmaskmax_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<SideTable>[]StripedMap 是一個模板類,在這個類中有一個 array 成員,用來存儲 PaddedT 對象,并且其中對于 [] 符的重載定義中,會返回這個 PaddedT 的 value 成員,這個 value 就是我們傳入的 T 泛型成員,也就是 SideTable 對象。在 array 的下標中,這里使用了 indexForPointer 方法通過位運算計算下標,實現了靜態的 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 根據首對象查找 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 函數把心的對象進行注冊操作,完成與對應的弱引用表進行綁定操作。

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

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

weaktable-2

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

以上就是對于 weak 弱引用對象的初始化時 runtime 內部的執行過程,想必閱讀后會對其結構有更深的理解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容