在整理復習 runtime 知識點的過程中,發現不得不鞏固 runtime 關于數據結構方面的知識,所以單獨開篇關于 NSObject 文章
目錄
準備: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 語言中創建的所有類基類,絕大部分都是繼承自 NSObject(NSProxy除外,上文已經有過說明,runtime的那些事(一)——runtime基礎介紹。因此想要深入學習 iOS 底層知識,NSObject 類拿來開刀再合適不過了(一臉正經:哈哈哈(?ω?)hiahiahia)
首先,進入查看 NSObject 類結構
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
?過濾掉 clang 命令的忽略警告代碼,其作用為忽略不推薦使用接口中的實例變量聲明
(關于 clang diagnostic 處理警告用法,可查詢clang.llvm.org提供的文檔說明,發現 NSObject 類只有一個實例變量Class isa
,而Class
定義為typedef struct objc_class *Class;
,作用為指向objc_class
的指針。
runtime 源碼準備
?如果繼續深入關于objc_class
的數據結構,就不能僅僅通過 Xcode 查看,因為在 Xcode 中提供給我們的 runtime API,是已經被廢棄的 Legacy 版本,若是想要查看現行使用的 Modern 版本,則可以從 Apple開源項目鏈接 查看下載最新版本,寫此文章時,runtime 最新版本為 objc4-750.1
。但直接下載的 runtime 源碼是無法在 Xcode 編譯通過,而且若系統升級到macOS Mojave,則只能使用 obj4_750 版本,舊版本會報錯。關于可編譯runtime源碼,直接從該鏈接下載Runtime源碼objc4-750編譯
回到正題,有了 runtime 的源碼,就可以看到現行 Objective-C 2.0 版本關于objc_class 結構體組成
?在結構體里,objc_class
繼承自objc_object
,意味著 class 本身在 runtime 中被作為對象來處理。而且objc_object
本身也是一個 struct 結構體。objc_class 結構體的完整聲明函數占據了300行代碼。其中有幾個最基礎、最關鍵的屬性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); }
該結構體使用C++代碼聲明,對C語言本身做了擴展,該結構體中可包含函數聲明。
1. objc_object
objc_class
繼承自 objc_object
,objc_object
中存在一個 isa
指針,因此 objc_class
也擁有自己的 isa
指針。在 Objective-C 語言中,所有的對象都會擁有一個 isa
指針,指針指向當前對象所屬的類,通過 isa
可在運行時當前對象的所屬類。關于 isa
指針,這篇 isa的本質 文章個人認為是解釋最全面細致的。
2. Class superclass
Class superclass;
,此處就是消息執行流程向父類傳遞最重要的實現屬性,代表著作為當前類的父類
3. class_data_bits_t bits
?class_data_bits_t bits;
,objc_class
結構體的核心,用于存儲類的屬性、方法、遵循的協議等各種信息。其本質是一個可被 Mask 標記的指針類型,根據不同 Mask,取出對應不同值。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
?在該結構體聲明 bits 的右側,runtime 注釋了 bits 相當于 class_rw_t
結構體加上 rr/alloc 的flag標記
?bits 只有一個成員
uintptr_t bits;
,此處 bits 不僅包含了指針,也記錄了Class
本身各種異或flag,用于聲明 Class
的屬性。將上述類的各種信息僅用一個 uint 指針復合到一起表示,可以理解成是一個復合指針
。當按需取出各類不同那個信息時,通過以
FAST_
前綴開頭的 flag 掩碼對 bits 進行按位與操作。
在寫文章過程中不斷出現早已變陌生的知識點,自己看著也是頭暈,決定一步一步消化掉
(1). 如何通過一個 uint 指針獲取類中各種不同信息?
?runtime 中已經聲明 class_data_bits_t bits
對于 data 數據讀取維護,基于 class_rw_t *
的結構體數據進行。執行 class_data_bits_t bits
結構體或者 objc_class
中的 data()
方法,會返回同一個 class_rw_t *
指針。
首先,要了解 class_data_bits_t bits
在內存中不同系統架構存在不同的位排列方式:
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);
}
?當通過 data()
方法讀取 class_rw_t *
指針數據時,runtime 代碼會添加一個 FAST_DATA_MASK
宏定義判斷,為啥要加這個宏定義?FAST_DATA_MASK
的宏定義如下
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
?使用MacOS自帶的計算器,將上述十六進制轉換成二進制后:
?可以發現,
class_rw_t
指針在 class_data_bits_t
結構體中真正存儲的位是 從第3位至46位
,這樣也能正好驗證了在64位兼容與不兼容的系統架構下,FAST_DATA_MASK
的位范圍是 3-46。?關于在 32 位與 64 位不同系統架構下的其它宏定義,有興趣的話,可以通過計算器一一驗證 runtime 中掩碼宏定義列表中的位數。
?關于其它的掩碼宏定義,可去 runtime 源碼中
objc-runtime-new.h
類文件的 372 - 525 行代碼查看。
(2). class_rw_t
接下來,繼續深入,剛才已經得知 class_data_bits_t *bits
結構體中真正存儲類信息的是 class_rw_t
,看下其中的數據結構
可以看到,類中的屬性、方法、遵循的協議都以
二維數組
的形式存儲,都是可讀寫屬性,其中包含了類的初始信息(來源于 class_ro_t
類型的常量指針)、以及分類的信息。設置成可寫屬性,為的是在運行時將該類的多個分類信息(包括屬性、方法、協議等)合并至類對應的二維數組中。還有兩個
Class
類的成員變量,分別代表著第一個子類、下一個分類,還有一個使用 const
修飾的 class_ro_t
常量指針(下面會介紹)
(3). class_ro_t
關于內部結構,直接貼代碼
發現該結構體和
class_rw_t
非常相似,但作用卻不同。在編譯期完成類的原始信息存儲,并用 const
修飾代表常量,不可再進行寫入修改。class_ro_t
在編譯期具體做了什么事?
- 類的結構體
class_data_bits_t
指向了class_ro_t
指針; - 類的屬性、方法、遵循協議數組都是在編譯期就已經確定(不包括分類信息),為只讀屬性,存儲于
class_ro_t
; - 類定義的實例化方法會添加至
class_ro_t
的baseMethodList
中
?換句話說,class_rw_t
不同于 class_ro_t
,在運行時動態將類的分類信息加入對應數組中,為類提供了很好的擴展能力,這也印證了 Objective-C 動態語言的特性。
4. cache_t cache
?發送消息時若每次從方法列表中去查找,性能會發生損耗,并且類存在繼承關系時,方法查找鏈會更長,損耗更嚴重,而 cache_t cache;
正是為了解決方法查找所引發的性能問題。通過散列表形式緩存調用過的方法函數,大幅提高訪問速度。
-
struct bucket_t *_buckets;
,是其核心部分,通過散列表來實現,并以key
與對應IMP
來存儲的緩存節點 -
mask_t _mask;
,代表用來分配緩存bucket
總數-1 -
mask_t _occupied;
,代表當前已實際占用的緩存bucket
數量
?此處又碰到了一個mask_t
的類型聲明,查看后發現是一個通過 typedef 定義的數據類型,uint32_t
代表32位無符號類型的數據,uint64_t
代表64位無符號類型的數據。
mask_t聲明
接下來就看下bucket_t
類型的組成
bucket_t聲明
cache_key_t _key
代表@selector
的方法名稱
IMP _imp
代表函數的存儲地址
?在public
中,可以發現對key
與對應IMP
的存儲過程,此處通過C++代碼分別實現了Key
與IMP
的 set 與 get 方法,并通過void bucket_t::set(cache_key_t newKey, IMP newImp)
函數方法完成賦值。
void bucket_t::set(cache_key_t newKey, IMP newImp)方法實現
在該實現方法中,我理解的賦值流程是,
?1. 當_key
值為0或者_key
內容(即selector方法名稱)與傳參newKey
相同時,不再進行下一步操作、
?2.newImp
直接賦值給_imp
?3. 當_key
與newKey
內容不相等時,會將newKey
賦值給_key
。
在第3步執行前,先去執行了mega_barrier()
宏定義,為什么要先執行該函數再去賦值_key
?
習慣性的點進了mega_barrier()
宏定義聲明,然后是一臉懵。。。
mega_barrier()聲明
?但我不甘心就此止步,于是 Google 了半天,最后在早已關注的歐陽大哥簡書深入解構objc_msgSend函數的實現文章找到了答案。
?原來此處使用了編譯內存屏障(Compiler Memory Barrier)技術,使用的原因是:因為程序在運行時內存實際的訪問順序與程序代碼編寫訪問順序不保證一致,即內存亂序訪問(內存亂序訪問的初衷是為了提升程序運行時性能),因此添加mega_barrier()
確保內存訪問順序與代碼編寫訪問順序一致。此處若不添加mega_barrier()
函數,則可能會造成先執行了_key
的賦值,再執行_imp
的賦值問題。
cache 查找過程:(以對象方法為例)
?(1). 通過isa
查找到指定 class
?(2). 從 cache 中查找,若存在緩存,則直接調用
?(3). 若緩存中不存在方法,則在自己的 class 里 bits 的 rw 中查找方法
?(4). 若找到該方法則調用,并將方法緩存至cache中
?(5). 若沒有找到,則通過 superclass 找到父類,繼續從父類class里 bits 的 rw 中查找方法
?(6). 若在父類中找到,則直接調用,并將方法緩存至自己 class 中;若找不到,則一直向上查找
內部 cache 原理因篇幅限制,會再開一篇新文章分析。
5. realizeClass
?這里單獨把 realizeClass
提溜出來,主要是用于類首次初始化流程,其重要性不言而喻。
?相對于在運行時,對于類信息的處理,主要依靠于 realizeClass
函數來實現。這里僅僅是介紹下 realizeClass
函數內部實現,關于類的初始化流程放在后續文章中。
附上結構體源代碼
在源代碼中有這樣一段注釋,翻譯過來就是:
?
realizeClass
,核心作用是對類進行首次初始化,其中包括分配讀寫數據內存空間,返回類的實際類結構。還有最后一句:鎖定狀態,runtimeLock必須由調用方進行寫入鎖定其中的主要作用代碼:
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
類型指針,并強制轉換成class_ro_t
類型指針賦值給ro
。 - 判斷若是普通的類,rw數據已經
allocated
分配了空間,則初始化一個class_rw_t
類型的結構體rw
。 - 對
rw
中ro
屬性進行指向第一步中被強制轉換的ro
指針操作, 并對flags
屬性進行位移操作,此處位移作用:表明當前類已開始實現但未完成或已完成實現。 - 最終將經過修改的
rw
設置為class_data_bits_t *bits
的 data 值,即objc_class
中最終完整的類結構數據。
?在上述流程執行前,realizeClass 執行了 runtimeLock.assertWriting();
代碼,我個人理解的代碼作用,是對數據的寫入進行了線程保護,并且由調用方(即函數的入參Class對象)進行寫入鎖定操作,保障數據寫入安全。
? runtime 類的運行邏輯:在編譯時,類的方法、屬性、協議等信息都存在于常量 class_ro_t
中,且無法再進行更改,這時class_data_bits_t
中通過 data()
方法獲取數據指向的是 class_ro_t
。到了運行時,類就能夠動態創建 class_rw_t
指針并將 class_ro_t
中的信息存儲,同時會將類的分類信息(包括:分類中的方法、屬性、協議等)一并存儲。通過二維數組進行排序,將分類信息放入數組前端,class_ro_t
中已有類信息放入數組后端。此時,class_data_bits_t
通過 data()
方法指針由 class_ro_t
變成了指向 class_rw_t
。以上的操作,是通過 realizeClass
函數來實現的。
上面所寫的,是對 NSObject 類的結構分析,文章初衷是計劃把 IMP 、NSInvocation、以及 NSObject 類初始化流程等 runtime 知識點都囊括,作為一個總結。但 runtime 的內容真的不是一兩篇就可以寫完的,寫作過程中發現僅僅是 NSObject 的數據結構介紹就占用了這么多篇幅。下一篇準備寫下 NSObject 類在初始化流程。
該文章首次發表在 簡書:我只不過是出來寫寫代碼 博客,并自動同步至 騰訊云:我只不過是出來寫寫iOS 博客