在整理復(fù)習(xí) runtime 知識(shí)點(diǎn)的過程中,發(fā)現(xiàn)不得不鞏固 runtime 關(guān)于數(shù)據(jù)結(jié)構(gòu)方面的知識(shí),所以單獨(dú)開篇關(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 底層知識(shí),NSObject 類拿來開刀再合適不過了(一臉正經(jīng):哈哈哈(?ω?)hiahiahia)
首先,進(jìn)入查看 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 命令的忽略警告代碼,其作用為忽略不推薦使用接口中的實(shí)例變量聲明(關(guān)于 clang diagnostic 處理警告用法,可查詢clang.llvm.org提供的文檔說明,發(fā)現(xiàn) NSObject 類只有一個(gè)實(shí)例變量Class isa,而Class定義為typedef struct objc_class *Class;,作用為指向objc_class的指針。
runtime 源碼準(zhǔn)備
?如果繼續(xù)深入關(guān)于objc_class的數(shù)據(jù)結(jié)構(gòu),就不能僅僅通過 Xcode 查看,因?yàn)樵?Xcode 中提供給我們的 runtime API,是已經(jīng)被廢棄的 Legacy 版本,若是想要查看現(xiàn)行使用的 Modern 版本,則可以從 Apple開源項(xiàng)目鏈接 查看下載最新版本,寫此文章時(shí),runtime 最新版本為 objc4-750.1。但直接下載的 runtime 源碼是無法在 Xcode 編譯通過,而且若系統(tǒng)升級(jí)到macOS Mojave,則只能使用 obj4_750 版本,舊版本會(huì)報(bào)錯(cuò)。關(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 中被作為對(duì)象來處理。而且objc_object本身也是一個(gè) struct 結(jié)構(gòu)體。objc_class 結(jié)構(gòu)體的完整聲明函數(shù)占據(jù)了300行代碼。其中有幾個(gè)最基礎(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)體使用C++代碼聲明,對(duì)C語言本身做了擴(kuò)展,該結(jié)構(gòu)體中可包含函數(shù)聲明。
1. objc_object
objc_class 繼承自 objc_object,objc_object 中存在一個(gè) isa 指針,因此 objc_class 也擁有自己的 isa 指針。在 Objective-C 語言中,所有的對(duì)象都會(huì)擁有一個(gè) isa 指針,指針指向當(dāng)前對(duì)象所屬的類,通過 isa 可在運(yùn)行時(shí)當(dāng)前對(duì)象的所屬類。關(guān)于 isa 指針,這篇 isa的本質(zhì) 文章個(gè)人認(rèn)為是解釋最全面細(xì)致的。

2. Class superclass
Class superclass;,此處就是消息執(zhí)行流程向父類傳遞最重要的實(shí)現(xiàn)屬性,代表著作為當(dāng)前類的父類
3. class_data_bits_t bits
?class_data_bits_t bits;,objc_class結(jié)構(gòu)體的核心,用于存儲(chǔ)類的屬性、方法、遵循的協(xié)議等各種信息。其本質(zhì)是一個(gè)可被 Mask 標(biāo)記的指針類型,根據(jù)不同 Mask,取出對(duì)應(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)記

?bits 只有一個(gè)成員
uintptr_t bits;,此處 bits 不僅包含了指針,也記錄了Class本身各種異或flag,用于聲明 Class 的屬性。將上述類的各種信息僅用一個(gè) uint 指針復(fù)合到一起表示,可以理解成是一個(gè)復(fù)合指針。當(dāng)按需取出各類不同那個(gè)信息時(shí),通過以
FAST_前綴開頭的 flag 掩碼對(duì) bits 進(jìn)行按位與操作。
在寫文章過程中不斷出現(xiàn)早已變陌生的知識(shí)點(diǎn),自己看著也是頭暈,決定一步一步消化掉
(1). 如何通過一個(gè) uint 指針獲取類中各種不同信息?
?runtime 中已經(jīng)聲明 class_data_bits_t bits 對(duì)于 data 數(shù)據(jù)讀取維護(hù),基于 class_rw_t * 的結(jié)構(gòu)體數(shù)據(jù)進(jìn)行。執(zhí)行 class_data_bits_t bits 結(jié)構(gòu)體或者 objc_class 中的 data() 方法,會(huì)返回同一個(gè) 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ù)時(shí),runtime 代碼會(huì)添加一個(gè) FAST_DATA_MASK 宏定義判斷,為啥要加這個(gè)宏定義?FAST_DATA_MASK 的宏定義如下
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
?使用MacOS自帶的計(jì)算器,將上述十六進(jìn)制轉(zhuǎn)換成二進(jìn)制后:

?可以發(fā)現(xiàn),
class_rw_t 指針在 class_data_bits_t 結(jié)構(gòu)體中真正存儲(chǔ)的位是 從第3位至46位,這樣也能正好驗(yàn)證了在64位兼容與不兼容的系統(tǒng)架構(gòu)下,FAST_DATA_MASK 的位范圍是 3-46。?關(guān)于在 32 位與 64 位不同系統(tǒng)架構(gòu)下的其它宏定義,有興趣的話,可以通過計(jì)算器一一驗(yàn)證 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)體中真正存儲(chǔ)類信息的是 class_rw_t,看下其中的數(shù)據(jù)結(jié)構(gòu)

可以看到,類中的屬性、方法、遵循的協(xié)議都以
二維數(shù)組 的形式存儲(chǔ),都是可讀寫屬性,其中包含了類的初始信息(來源于 class_ro_t 類型的常量指針)、以及分類的信息。設(shè)置成可寫屬性,為的是在運(yùn)行時(shí)將該類的多個(gè)分類信息(包括屬性、方法、協(xié)議等)合并至類對(duì)應(yīng)的二維數(shù)組中。還有兩個(gè)
Class 類的成員變量,分別代表著第一個(gè)子類、下一個(gè)分類,還有一個(gè)使用 const 修飾的 class_ro_t 常量指針(下面會(huì)介紹)
(3). class_ro_t
關(guān)于內(nèi)部結(jié)構(gòu),直接貼代碼

發(fā)現(xiàn)該結(jié)構(gòu)體和
class_rw_t 非常相似,但作用卻不同。在編譯期完成類的原始信息存儲(chǔ),并用 const 修飾代表常量,不可再進(jìn)行寫入修改。class_ro_t 在編譯期具體做了什么事?
- 類的結(jié)構(gòu)體
class_data_bits_t指向了class_ro_t指針; - 類的屬性、方法、遵循協(xié)議數(shù)組都是在編譯期就已經(jīng)確定(不包括分類信息),為只讀屬性,存儲(chǔ)于
class_ro_t; - 類定義的實(shí)例化方法會(huì)添加至
class_ro_t的baseMethodList中
?換句話說,class_rw_t 不同于 class_ro_t,在運(yùn)行時(shí)動(dòng)態(tài)將類的分類信息加入對(duì)應(yīng)數(shù)組中,為類提供了很好的擴(kuò)展能力,這也印證了 Objective-C 動(dòng)態(tài)語言的特性。
4. cache_t cache
?發(fā)送消息時(shí)若每次從方法列表中去查找,性能會(huì)發(fā)生損耗,并且類存在繼承關(guān)系時(shí),方法查找鏈會(huì)更長(zhǎng),損耗更嚴(yán)重,而 cache_t cache; 正是為了解決方法查找所引發(fā)的性能問題。通過散列表形式緩存調(diào)用過的方法函數(shù),大幅提高訪問速度。

-
struct bucket_t *_buckets;,是其核心部分,通過散列表來實(shí)現(xiàn),并以key與對(duì)應(yīng)IMP來存儲(chǔ)的緩存節(jié)點(diǎn) -
mask_t _mask;,代表用來分配緩存bucket總數(shù)-1 -
mask_t _occupied;,代表當(dāng)前已實(shí)際占用的緩存bucket數(shù)量
?此處又碰到了一個(gè)mask_t的類型聲明,查看后發(fā)現(xiàn)是一個(gè)通過 typedef 定義的數(shù)據(jù)類型,uint32_t代表32位無符號(hào)類型的數(shù)據(jù),uint64_t代表64位無符號(hào)類型的數(shù)據(jù)。
mask_t聲明
接下來就看下bucket_t類型的組成
bucket_t聲明
cache_key_t _key代表@selector的方法名稱
IMP _imp代表函數(shù)的存儲(chǔ)地址
?在public中,可以發(fā)現(xiàn)對(duì)key與對(duì)應(yīng)IMP的存儲(chǔ)過程,此處通過C++代碼分別實(shí)現(xiàn)了Key與IMP的 set 與 get 方法,并通過void bucket_t::set(cache_key_t newKey, IMP newImp)函數(shù)方法完成賦值。
void bucket_t::set(cache_key_t newKey, IMP newImp)方法實(shí)現(xiàn)
在該實(shí)現(xiàn)方法中,我理解的賦值流程是,
?1. 當(dāng)_key值為0或者_key內(nèi)容(即selector方法名稱)與傳參newKey相同時(shí),不再進(jìn)行下一步操作、
?2.newImp直接賦值給_imp
?3. 當(dāng)_key與newKey內(nèi)容不相等時(shí),會(huì)將newKey賦值給_key。
在第3步執(zhí)行前,先去執(zhí)行了mega_barrier()宏定義,為什么要先執(zhí)行該函數(shù)再去賦值_key?
習(xí)慣性的點(diǎn)進(jìn)了mega_barrier()宏定義聲明,然后是一臉懵。。。
mega_barrier()聲明
?但我不甘心就此止步,于是 Google 了半天,最后在早已關(guān)注的歐陽大哥簡(jiǎn)書深入解構(gòu)objc_msgSend函數(shù)的實(shí)現(xiàn)文章找到了答案。
?原來此處使用了編譯內(nèi)存屏障(Compiler Memory Barrier)技術(shù),使用的原因是:因?yàn)槌绦蛟谶\(yùn)行時(shí)內(nèi)存實(shí)際的訪問順序與程序代碼編寫訪問順序不保證一致,即內(nèi)存亂序訪問(內(nèi)存亂序訪問的初衷是為了提升程序運(yùn)行時(shí)性能),因此添加mega_barrier()確保內(nèi)存訪問順序與代碼編寫訪問順序一致。此處若不添加mega_barrier()函數(shù),則可能會(huì)造成先執(zhí)行了_key的賦值,再執(zhí)行_imp的賦值問題。
cache 查找過程:(以對(duì)象方法為例)
?(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 原理因篇幅限制,會(huì)再開一篇新文章分析。
5. realizeClass
?這里單獨(dú)把 realizeClass 提溜出來,主要是用于類首次初始化流程,其重要性不言而喻。
?相對(duì)于在運(yùn)行時(shí),對(duì)于類信息的處理,主要依靠于 realizeClass 函數(shù)來實(shí)現(xiàn)。這里僅僅是介紹下 realizeClass 函數(shù)內(nèi)部實(shí)現(xiàn),關(guān)于類的初始化流程放在后續(xù)文章中。
附上結(jié)構(gòu)體源代碼

在源代碼中有這樣一段注釋,翻譯過來就是:
?
realizeClass,核心作用是對(duì)類進(jìn)行首次初始化,其中包括分配讀寫數(shù)據(jù)內(nèi)存空間,返回類的實(shí)際類結(jié)構(gòu)。還有最后一句:鎖定狀態(tài),runtimeLock必須由調(diào)用方進(jìn)行寫入鎖定其中的主要作用代碼:
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類型指針,并強(qiáng)制轉(zhuǎn)換成class_ro_t類型指針賦值給ro。 - 判斷若是普通的類,rw數(shù)據(jù)已經(jīng)
allocated分配了空間,則初始化一個(gè)class_rw_t類型的結(jié)構(gòu)體rw。 - 對(duì)
rw中ro屬性進(jìn)行指向第一步中被強(qiáng)制轉(zhuǎn)換的ro指針操作, 并對(duì)flags屬性進(jìn)行位移操作,此處位移作用:表明當(dāng)前類已開始實(shí)現(xiàn)但未完成或已完成實(shí)現(xiàn)。 - 最終將經(jīng)過修改的
rw設(shè)置為class_data_bits_t *bits的 data 值,即objc_class中最終完整的類結(jié)構(gòu)數(shù)據(jù)。
?在上述流程執(zhí)行前,realizeClass 執(zhí)行了 runtimeLock.assertWriting(); 代碼,我個(gè)人理解的代碼作用,是對(duì)數(shù)據(jù)的寫入進(jìn)行了線程保護(hù),并且由調(diào)用方(即函數(shù)的入?yún)lass對(duì)象)進(jìn)行寫入鎖定操作,保障數(shù)據(jù)寫入安全。
? runtime 類的運(yùn)行邏輯:在編譯時(shí),類的方法、屬性、協(xié)議等信息都存在于常量 class_ro_t 中,且無法再進(jìn)行更改,這時(shí)class_data_bits_t中通過 data() 方法獲取數(shù)據(jù)指向的是 class_ro_t 。到了運(yùn)行時(shí),類就能夠動(dòng)態(tài)創(chuàng)建 class_rw_t 指針并將 class_ro_t 中的信息存儲(chǔ),同時(shí)會(huì)將類的分類信息(包括:分類中的方法、屬性、協(xié)議等)一并存儲(chǔ)。通過二維數(shù)組進(jìn)行排序,將分類信息放入數(shù)組前端,class_ro_t 中已有類信息放入數(shù)組后端。此時(shí),class_data_bits_t 通過 data() 方法指針由 class_ro_t 變成了指向 class_rw_t 。以上的操作,是通過 realizeClass 函數(shù)來實(shí)現(xiàn)的。
上面所寫的,是對(duì) NSObject 類的結(jié)構(gòu)分析,文章初衷是計(jì)劃把 IMP 、NSInvocation、以及 NSObject 類初始化流程等 runtime 知識(shí)點(diǎn)都囊括,作為一個(gè)總結(jié)。但 runtime 的內(nèi)容真的不是一兩篇就可以寫完的,寫作過程中發(fā)現(xiàn)僅僅是 NSObject 的數(shù)據(jù)結(jié)構(gòu)介紹就占用了這么多篇幅。下一篇準(zhǔn)備寫下 NSObject 類在初始化流程。
該文章首次發(fā)表在 簡(jiǎn)書:我只不過是出來寫寫代碼 博客,并自動(dòng)同步至 騰訊云:我只不過是出來寫寫iOS 博客



