runtime的那些事(二)——NSObject數(shù)據(jù)結(jié)構(gòu)

在整理復(fù)習(xí) runtime 知識點的過程中,發(fā)現(xiàn)不得不鞏固 runtime 關(guān)于數(shù)據(jù)結(jié)構(gòu)方面的知識漫拭,所以單獨開篇關(guān)于 NSObject 文章

目錄


準(zhǔn)備:runtime 源碼

1. objc_object

2. Class superclass

3. class_data_bits_t bits

?(1). class_data_bits_t bits 掩碼取值

?(2). class_rw_t

?(3). class_ro_t

4. cache_t cache

5. realizeClass


正文

?在使用 Objective-C 語言中創(chuàng)建的所有類基類采驻,絕大部分都是繼承自 NSObject(NSProxy除外,上文已經(jīng)有過說明礼旅,runtime的那些事(一)——runtime基礎(chǔ)介紹痘系。因此想要深入學(xué)習(xí) iOS 底層知識,NSObject 類拿來開刀再合適不過了(一臉正經(jīng):哈哈哈(?ω?)hiahiahia)
首先龄坪,進入查看 NSObject 類結(jié)構(gòu)

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

?過濾掉 clang 命令的忽略警告代碼奴璃,其作用為忽略不推薦使用接口中的實例變量聲明(關(guān)于 clang diagnostic 處理警告用法,可查詢clang.llvm.org提供的文檔說明抄课,發(fā)現(xiàn) NSObject 類只有一個實例變量Class isa,而Class定義為typedef struct objc_class *Class;间聊,作用為指向objc_class的指針抵拘。

runtime 源碼準(zhǔn)備

?如果繼續(xù)深入關(guān)于objc_class的數(shù)據(jù)結(jié)構(gòu),就不能僅僅通過 Xcode 查看尚蝌,因為在 Xcode 中提供給我們的 runtime API充尉,是已經(jīng)被廢棄的 Legacy 版本,若是想要查看現(xiàn)行使用的 Modern 版本姿鸿,則可以從 Apple開源項目鏈接 查看下載最新版本倒源,寫此文章時,runtime 最新版本為 objc4-750.1热某。但直接下載的 runtime 源碼是無法在 Xcode 編譯通過突诬,而且若系統(tǒng)升級到macOS Mojave芜繁,則只能使用 obj4_750 版本骏令,舊版本會報錯。關(guān)于可編譯runtime源碼榔袋,直接從該鏈接下載Runtime源碼objc4-750編譯
回到正題凰兑,有了 runtime 的源碼妥粟,就可以看到現(xiàn)行 Objective-C 2.0 版本關(guān)于objc_class 結(jié)構(gòu)體組成
?在結(jié)構(gòu)體里,objc_class繼承自objc_object吏够,意味著 class 本身在 runtime 中被作為對象來處理勾给。而且objc_object本身也是一個 struct 結(jié)構(gòu)體滩报。objc_class 結(jié)構(gòu)體的完整聲明函數(shù)占據(jù)了300行代碼。其中有幾個最基礎(chǔ)播急、最關(guān)鍵的屬性Class superclass;脓钾、cache_t cache;class_data_bits_t bits;桩警、class_rw_t *data() { return bits.data(); }可训、void setData(class_rw_t *newData) { bits.setData(newData); }

結(jié)構(gòu)體聲明截圖

該結(jié)構(gòu)體使用C++代碼聲明,對C語言本身做了擴展捶枢,該結(jié)構(gòu)體中可包含函數(shù)聲明。


1. objc_object

objc_class 繼承自 objc_object烂叔,objc_object 中存在一個 isa 指針川蒙,因此 objc_class 也擁有自己的 isa 指針。在 Objective-C 語言中长已,所有的對象都會擁有一個 isa 指針畜眨,指針指向當(dāng)前對象所屬的類术瓮,通過 isa 可在運行時當(dāng)前對象的所屬類康聂。關(guān)于 isa 指針,這篇 isa的本質(zhì) 文章個人認(rèn)為是解釋最全面細(xì)致的胞四。

objc_object結(jié)構(gòu)體


2. Class superclass

Class superclass;恬汁,此處就是消息執(zhí)行流程向父類傳遞最重要的實現(xiàn)屬性,代表著作為當(dāng)前類的父類


3. class_data_bits_t bits

?class_data_bits_t bits;辜伟,objc_class結(jié)構(gòu)體的核心氓侧,用于存儲類的屬性、方法导狡、遵循的協(xié)議等各種信息约巷。其本質(zhì)是一個可被 Mask 標(biāo)記的指針類型,根據(jù)不同 Mask旱捧,取出對應(yīng)不同值独郎。

    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

?在該結(jié)構(gòu)體聲明 bits 的右側(cè),runtime 注釋了 bits 相當(dāng)于 class_rw_t 結(jié)構(gòu)體加上 rr/alloc 的flag標(biāo)記

class_data_bits_t 結(jié)構(gòu)體聲明

?bits 只有一個成員 uintptr_t bits;枚赡,此處 bits 不僅包含了指針氓癌,也記錄了Class本身各種異或flag,用于聲明 Class 的屬性贫橙。將上述類的各種信息僅用一個 uint 指針復(fù)合到一起表示贪婉,可以理解成是一個復(fù)合指針
當(dāng)按需取出各類不同那個信息時卢肃,通過以FAST_前綴開頭的 flag 掩碼對 bits 進行按位與操作疲迂。

在寫文章過程中不斷出現(xiàn)早已變陌生的知識點星压,自己看著也是頭暈,決定一步一步消化掉

(1). 如何通過一個 uint 指針獲取類中各種不同信息鬼譬?

?runtime 中已經(jīng)聲明 class_data_bits_t bits 對于 data 數(shù)據(jù)讀取維護娜膘,基于 class_rw_t * 的結(jié)構(gòu)體數(shù)據(jù)進行。執(zhí)行 class_data_bits_t bits 結(jié)構(gòu)體或者 objc_class 中的 data() 方法优质,會返回同一個 class_rw_t * 指針竣贪。
首先,要了解 class_data_bits_t bits 在內(nèi)存中不同系統(tǒng)架構(gòu)存在不同的位排列方式:
32位

0 1 2-31
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_DATA_MASK

64位兼容

0 1 2 3-46 47-63
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_REQUIRES_RAW_ISA FAST_DATA_MASK 空閑

64位不兼容

0 1 2 3-46 47
FAST_IS_SWIFT FAST_REQUIRES_RAW_ISA FAST_HAS_CXX_DTOR FAST_DATA_MASK FAST_HAS_CXX_CTOR
48 49 50 51 52-63
FAST_HAS_DEFAULT_AWZ FAST_HAS_DEFAULT_RR FAST_ALLOC FAST_SHIFTED_SIZE_SHIFT 空閑
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

?當(dāng)通過 data() 方法讀取 class_rw_t * 指針數(shù)據(jù)時巩螃,runtime 代碼會添加一個 FAST_DATA_MASK 宏定義判斷演怎,為啥要加這個宏定義?FAST_DATA_MASK 的宏定義如下

// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL

?使用MacOS自帶的計算器避乏,將上述十六進制轉(zhuǎn)換成二進制后:

轉(zhuǎn)換結(jié)果

?可以發(fā)現(xiàn)爷耀,class_rw_t 指針在 class_data_bits_t 結(jié)構(gòu)體中真正存儲的位是 從第3位至46位,這樣也能正好驗證了在64位兼容與不兼容的系統(tǒng)架構(gòu)下拍皮,FAST_DATA_MASK 的位范圍是 3-46歹叮。
?關(guān)于在 32 位與 64 位不同系統(tǒng)架構(gòu)下的其它宏定義,有興趣的話铆帽,可以通過計算器一一驗證 runtime 中掩碼宏定義列表中的位數(shù)咆耿。
?關(guān)于其它的掩碼宏定義,可去 runtime 源碼中 objc-runtime-new.h 類文件的 372 - 525 行代碼查看爹橱。

(2). class_rw_t

接下來萨螺,繼續(xù)深入,剛才已經(jīng)得知 class_data_bits_t *bits 結(jié)構(gòu)體中真正存儲類信息的是 class_rw_t愧驱,看下其中的數(shù)據(jù)結(jié)構(gòu)

class_rw_t數(shù)據(jù)結(jié)構(gòu)

可以看到慰技,類中的屬性、方法组砚、遵循的協(xié)議都以 二維數(shù)組 的形式存儲吻商,都是可讀寫屬性,其中包含了類的初始信息(來源于 class_ro_t 類型的常量指針)惫确、以及分類的信息手报。設(shè)置成可寫屬性蚯舱,為的是在運行時將該類的多個分類信息(包括屬性改化、方法、協(xié)議等)合并至類對應(yīng)的二維數(shù)組中枉昏。
還有兩個 Class 類的成員變量陈肛,分別代表著第一個子類、下一個分類兄裂,還有一個使用 const 修飾的 class_ro_t 常量指針(下面會介紹)

(3). class_ro_t

關(guān)于內(nèi)部結(jié)構(gòu)句旱,直接貼代碼

class_ro_t

發(fā)現(xiàn)該結(jié)構(gòu)體和 class_rw_t 非常相似阳藻,但作用卻不同。在編譯期完成類的原始信息存儲谈撒,并用 const 修飾代表常量腥泥,不可再進行寫入修改。
class_ro_t 在編譯期具體做了什么事啃匿?

  • 類的結(jié)構(gòu)體 class_data_bits_t 指向了 class_ro_t 指針蛔外;
  • 類的屬性、方法溯乒、遵循協(xié)議數(shù)組都是在編譯期就已經(jīng)確定(不包括分類信息)夹厌,為只讀屬性,存儲于 class_ro_t裆悄;
  • 類定義的實例化方法會添加至 class_ro_tbaseMethodList

?換句話說矛纹,class_rw_t 不同于 class_ro_t,在運行時動態(tài)將類的分類信息加入對應(yīng)數(shù)組中光稼,為類提供了很好的擴展能力或南,這也印證了 Objective-C 動態(tài)語言的特性。


4. cache_t cache

?發(fā)送消息時若每次從方法列表中去查找艾君,性能會發(fā)生損耗迎献,并且類存在繼承關(guān)系時,方法查找鏈會更長腻贰,損耗更嚴(yán)重吁恍,而 cache_t cache; 正是為了解決方法查找所引發(fā)的性能問題。通過散列表形式緩存調(diào)用過的方法函數(shù)播演,大幅提高訪問速度冀瓦。

cache_t結(jié)構(gòu)體

  • struct bucket_t *_buckets;,是其核心部分写烤,通過散列表來實現(xiàn)翼闽,并以key與對應(yīng)IMP來存儲的緩存節(jié)點
  • mask_t _mask;,代表用來分配緩存bucket 總數(shù)-1
  • mask_t _occupied;洲炊,代表當(dāng)前已實際占用的緩存bucket數(shù)量
    ?此處又碰到了一個mask_t的類型聲明感局,查看后發(fā)現(xiàn)是一個通過 typedef 定義的數(shù)據(jù)類型,uint32_t代表32位無符號類型的數(shù)據(jù)暂衡,uint64_t代表64位無符號類型的數(shù)據(jù)询微。
    mask_t聲明

    接下來就看下bucket_t類型的組成
    bucket_t聲明

    cache_key_t _key代表@selector的方法名稱
    IMP _imp代表函數(shù)的存儲地址
    ?在public中,可以發(fā)現(xiàn)對key與對應(yīng)IMP的存儲過程狂巢,此處通過C++代碼分別實現(xiàn)了KeyIMP的 set 與 get 方法撑毛,并通過void bucket_t::set(cache_key_t newKey, IMP newImp)函數(shù)方法完成賦值。
    void bucket_t::set(cache_key_t newKey, IMP newImp)方法實現(xiàn)

    在該實現(xiàn)方法中唧领,我理解的賦值流程是藻雌,
    ?1. 當(dāng)_key值為0或者_key內(nèi)容(即selector方法名稱)與傳參newKey相同時雌续,不再進行下一步操作、
    ?2. newImp直接賦值給_imp
    ?3. 當(dāng)_keynewKey內(nèi)容不相等時胯杭,會將newKey賦值給_key驯杜。
    在第3步執(zhí)行前,先去執(zhí)行了mega_barrier()宏定義做个,為什么要先執(zhí)行該函數(shù)再去賦值_key艇肴?
    習(xí)慣性的點進了mega_barrier()宏定義聲明,然后是一臉懵叁温。再悼。。
    mega_barrier()聲明

    ?但我不甘心就此止步膝但,于是 Google 了半天冲九,最后在早已關(guān)注的歐陽大哥簡書深入解構(gòu)objc_msgSend函數(shù)的實現(xiàn)文章找到了答案。
    ?原來此處使用了編譯內(nèi)存屏障(Compiler Memory Barrier)技術(shù)跟束,使用的原因是:因為程序在運行時內(nèi)存實際的訪問順序與程序代碼編寫訪問順序不保證一致莺奸,即內(nèi)存亂序訪問(內(nèi)存亂序訪問的初衷是為了提升程序運行時性能),因此添加 mega_barrier() 確保內(nèi)存訪問順序與代碼編寫訪問順序一致冀宴。此處若不添加mega_barrier()函數(shù)灭贷,則可能會造成先執(zhí)行了_key的賦值,再執(zhí)行_imp的賦值問題略贮。

cache 查找過程:(以對象方法為例)
?(1). 通過isa查找到指定 class
?(2). 從 cache 中查找甚疟,若存在緩存,則直接調(diào)用
?(3). 若緩存中不存在方法逃延,則在自己的 class 里 bits 的 rw 中查找方法
?(4). 若找到該方法則調(diào)用览妖,并將方法緩存至cache中
?(5). 若沒有找到,則通過 superclass 找到父類揽祥,繼續(xù)從父類class里 bits 的 rw 中查找方法
?(6). 若在父類中找到讽膏,則直接調(diào)用,并將方法緩存至自己 class 中拄丰;若找不到府树,則一直向上查找

內(nèi)部 cache 原理因篇幅限制,會再開一篇新文章分析料按。


5. realizeClass

?這里單獨把 realizeClass 提溜出來奄侠,主要是用于類首次初始化流程,其重要性不言而喻站绪。
?相對于在運行時遭铺,對于類信息的處理,主要依靠于 realizeClass 函數(shù)來實現(xiàn)恢准。這里僅僅是介紹下 realizeClass 函數(shù)內(nèi)部實現(xiàn)魂挂,關(guān)于類的初始化流程放在后續(xù)文章中。

附上結(jié)構(gòu)體源代碼

realizeClass函數(shù)部分代碼

在源代碼中有這樣一段注釋馁筐,翻譯過來就是:
?realizeClass涂召,核心作用是對類進行首次初始化,其中包括分配讀寫數(shù)據(jù)內(nèi)存空間敏沉,返回類的實際類結(jié)構(gòu)果正。還有最后一句:鎖定狀態(tài),runtimeLock必須由調(diào)用方進行寫入鎖定
其中的主要作用代碼:

    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }
  • 通過 data() 方法獲取到 class_rw_t 類型指針盟迟,并強制轉(zhuǎn)換成 class_ro_t 類型指針賦值給 ro秋泳。
  • 判斷若是普通的類,rw數(shù)據(jù)已經(jīng) allocated 分配了空間攒菠,則初始化一個 class_rw_t 類型的結(jié)構(gòu)體 rw迫皱。
  • rwro 屬性進行指向第一步中被強制轉(zhuǎn)換的 ro 指針操作, 并對 flags 屬性進行位移操作辖众,此處位移作用:表明當(dāng)前類已開始實現(xiàn)但未完成或已完成實現(xiàn)卓起。
  • 最終將經(jīng)過修改的 rw 設(shè)置為 class_data_bits_t *bits的 data 值,即 objc_class 中最終完整的類結(jié)構(gòu)數(shù)據(jù)凹炸。

?在上述流程執(zhí)行前戏阅,realizeClass 執(zhí)行了 runtimeLock.assertWriting(); 代碼,我個人理解的代碼作用啤它,是對數(shù)據(jù)的寫入進行了線程保護奕筐,并且由調(diào)用方(即函數(shù)的入?yún)lass對象)進行寫入鎖定操作,保障數(shù)據(jù)寫入安全变骡。

? runtime 類的運行邏輯:在編譯時救欧,類的方法、屬性锣光、協(xié)議等信息都存在于常量 class_ro_t 中笆怠,且無法再進行更改,這時class_data_bits_t中通過 data() 方法獲取數(shù)據(jù)指向的是 class_ro_t 誊爹。到了運行時蹬刷,類就能夠動態(tài)創(chuàng)建 class_rw_t 指針并將 class_ro_t 中的信息存儲,同時會將類的分類信息(包括:分類中的方法频丘、屬性办成、協(xié)議等)一并存儲。通過二維數(shù)組進行排序搂漠,將分類信息放入數(shù)組前端迂卢,class_ro_t 中已有類信息放入數(shù)組后端吓坚。此時,class_data_bits_t 通過 data() 方法指針由 class_ro_t 變成了指向 class_rw_t 宗挥。以上的操作维费,是通過 realizeClass 函數(shù)來實現(xiàn)的。


上面所寫的员萍,是對 NSObject 類的結(jié)構(gòu)分析腾降,文章初衷是計劃把 IMP 、NSInvocation碎绎、以及 NSObject 類初始化流程等 runtime 知識點都囊括螃壤,作為一個總結(jié)。但 runtime 的內(nèi)容真的不是一兩篇就可以寫完的筋帖,寫作過程中發(fā)現(xiàn)僅僅是 NSObject 的數(shù)據(jù)結(jié)構(gòu)介紹就占用了這么多篇幅奸晴。下一篇準(zhǔn)備寫下 NSObject 類在初始化流程。


該文章首次發(fā)表在 簡書:我只不過是出來寫寫代碼 博客日麸,并自動同步至 騰訊云:我只不過是出來寫寫iOS 博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寄啼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赘淮,更是在濱河造成了極大的恐慌辕录,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梢卸,死亡現(xiàn)場離奇詭異走诞,居然都是意外死亡,警方通過查閱死者的電腦和手機蛤高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門蚣旱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人戴陡,你說我怎么就攤上這事塞绿。” “怎么了恤批?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵异吻,是天一觀的道長。 經(jīng)常有香客問我喜庞,道長诀浪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任延都,我火速辦了婚禮雷猪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晰房。我一直安慰自己求摇,他們只是感情好射沟,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著与境,像睡著了一般验夯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嚷辅,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天簿姨,我揣著相機與錄音距误,去河邊找鬼簸搞。 笑死,一個胖子當(dāng)著我的面吹牛准潭,可吹牛的內(nèi)容都是我干的趁俊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼刑然,長吁一口氣:“原來是場噩夢啊……” “哼寺擂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起泼掠,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤怔软,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后择镇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挡逼,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年腻豌,在試婚紗的時候發(fā)現(xiàn)自己被綠了家坎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡吝梅,死狀恐怖虱疏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苏携,我是刑警寧澤做瞪,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站右冻,受9級特大地震影響装蓬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜国旷,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一矛物、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧跪但,春花似錦履羞、人聲如沸峦萎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽爱榔。三九已至,卻和暖如春糙及,著一層夾襖步出監(jiān)牢的瞬間详幽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工浸锨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留唇聘,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓柱搜,卻偏偏與公主長得像迟郎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子聪蘸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容