runtime的那些事(二)——NSObject數據結構

在整理復習 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_objectobjc_object 中存在一個 isa 指針,因此 objc_class 也擁有自己的 isa 指針。在 Objective-C 語言中,所有的對象都會擁有一個 isa 指針,指針指向當前對象所屬的類,通過 isa 可在運行時當前對象的所屬類。關于 isa 指針,這篇 isa的本質 文章個人認為是解釋最全面細致的。

objc_object結構體


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標記

class_data_bits_t 結構體聲明

?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_rw_t數據結構

可以看到,類中的屬性、方法、遵循的協議都以 二維數組 的形式存儲,都是可讀寫屬性,其中包含了類的初始信息(來源于 class_ro_t 類型的常量指針)、以及分類的信息。設置成可寫屬性,為的是在運行時將該類的多個分類信息(包括屬性、方法、協議等)合并至類對應的二維數組中。
還有兩個 Class 類的成員變量,分別代表著第一個子類、下一個分類,還有一個使用 const 修飾的 class_ro_t 常量指針(下面會介紹)

(3). class_ro_t

關于內部結構,直接貼代碼

class_ro_t

發現該結構體和 class_rw_t 非常相似,但作用卻不同。在編譯期完成類的原始信息存儲,并用 const 修飾代表常量,不可再進行寫入修改。
class_ro_t 在編譯期具體做了什么事?

  • 類的結構體 class_data_bits_t 指向了 class_ro_t 指針;
  • 類的屬性、方法、遵循協議數組都是在編譯期就已經確定(不包括分類信息),為只讀屬性,存儲于 class_ro_t
  • 類定義的實例化方法會添加至 class_ro_tbaseMethodList

?換句話說,class_rw_t 不同于 class_ro_t,在運行時動態將類的分類信息加入對應數組中,為類提供了很好的擴展能力,這也印證了 Objective-C 動態語言的特性。


4. cache_t cache

?發送消息時若每次從方法列表中去查找,性能會發生損耗,并且類存在繼承關系時,方法查找鏈會更長,損耗更嚴重,而 cache_t cache; 正是為了解決方法查找所引發的性能問題。通過散列表形式緩存調用過的方法函數,大幅提高訪問速度。

cache_t結構體

  • 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++代碼分別實現了KeyIMP的 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. 當_keynewKey內容不相等時,會將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函數部分代碼

在源代碼中有這樣一段注釋,翻譯過來就是:
?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
  • rwro 屬性進行指向第一步中被強制轉換的 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 博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容