在整理復(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)體使用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ì)致的胞四。
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)記
?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)換成二進制后:
?可以發(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)
可以看到慰技,類中的屬性、方法组砚、遵循的協(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)句旱,直接貼代碼
發(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_t
的baseMethodList
中
?換句話說矛纹,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ù)播演,大幅提高訪問速度冀瓦。
-
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ù)询微。
接下來就看下bucket_t
類型的組成
cache_key_t _key
代表@selector
的方法名稱
IMP _imp
代表函數(shù)的存儲地址
?在public
中,可以發(fā)現(xiàn)對key
與對應(yīng)IMP
的存儲過程狂巢,此處通過C++代碼分別實現(xiàn)了Key
與IMP
的 set 與 get 方法撑毛,并通過void bucket_t::set(cache_key_t newKey, IMP newImp)
函數(shù)方法完成賦值。
在該實現(xiàn)方法中唧领,我理解的賦值流程是藻雌,
?1. 當(dāng)_key
值為0或者_key
內(nèi)容(即selector方法名稱)與傳參newKey
相同時雌续,不再進行下一步操作、
?2.newImp
直接賦值給_imp
?3. 當(dāng)_key
與newKey
內(nèi)容不相等時胯杭,會將newKey
賦值給_key
驯杜。
在第3步執(zhí)行前,先去執(zhí)行了mega_barrier()
宏定義做个,為什么要先執(zhí)行該函數(shù)再去賦值_key
艇肴?
習(xí)慣性的點進了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ù)據(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
迫皱。 - 對
rw
中ro
屬性進行指向第一步中被強制轉(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 博客