iOS 底層探索系列編程
咱們在前面探索了 iOS
中的對象原理,面向對象編程中有一句名言:緩存
萬物皆對象
那麼對象又是從哪來的呢?有過面向對象編程基礎的同窗確定都知道是類派生出對象的,那麼今天咱們就一塊兒來探索一下類的底層原理吧。bash
iOS
中的類究竟是什麼?咱們在平常開發中大多數狀況都是從 NSObject
這個基類來派生出咱們須要的類。那麼在 OC
底層,咱們的類 Class
到底被編譯成什麼樣子了呢?數據結構
咱們新建一個 macOS
控制檯項目,而後新建一個 Animal
類出來。app
// Animal.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Animal : NSObject @end NS_ASSUME_NONNULL_END // Animal.m @implementation Animal @end // main.m #import <Foundation/Foundation.h> #import "Animal.h" int main(int argc, const char * argv[]) { @autoreleasepool { Animal *animal = [[Animal alloc] init]; NSLog(@"%p", animal); } return 0; }
咱們在終端執行 clang
命令:post
clang -rewrite-objc main.m -o main.cpp
這個命令是將咱們的 main.m
重寫成 main.cpp
,咱們打開這個文件搜索 Animal
:測試
咱們發現有多個地方都出現了 Animal
:ui
// 1 typedef struct objc_object Animal; // 2 struct Animal_IMPL { struct NSObject_IMPL NSObject_IVARS; }; // 3 objc_getClass("Animal")
咱們先全局搜索第一個 typedef struct objc_object
,發現有 843 個結果this
咱們經過 Command + G
快捷鍵快速翻閱一下,最終在 7626 行找到了 Class
的定義:atom
typedef struct objc_class *Class;
由這行代碼咱們能夠得出一個結論,Class
類型在底層是一個結構體類型的指針,這個結構體類型爲 objc_class
。
再搜索 typedef struct objc_class
發現搜不出來了,這個時候咱們須要在 objc4-756
源碼中進行探索了。
咱們在 objc4-756
源碼中直接搜索 struct objc_class
,而後定位到 objc-runtime-new.h
文件
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() { return bits.data(); } }
看到這裏,細心的讀者可能會發現,咱們在前面探索對象原理中遇到的 objc_object
再次出現了,而且此次是做爲 objc_class
的父類。這裏再次引用那句經典名言 萬物皆對象,也就是說類其實也是一種對象。
由此,咱們能夠簡單總結一下類和對象在 C
和 OC
中分別的定義
| C | OC |
| --- | --- |
| objc_object | NSObject |
| objc_class | NSObject(Class) |
經過上面的探索,咱們已經知道了類本質上也是對象,而平常開發中常見的成員變量、屬性、方法、協議等都是在類裏面存在的,那麼咱們是否是能夠猜測在 iOS
底層,類其實就存儲了這些內容呢?
咱們能夠經過分析源碼來驗證咱們的猜測。
從上一節中 objc_class
的定義處,咱們能夠梳理出 Class
中的 4 個屬性
isa
指針superclass
指針cache
bits
須要值得注意的是,這裏的
isa
指針在這裏是隱藏屬性.
isa
指針首先是 isa
指針,咱們以前已經探索過了,在對象初始化的時候,經過 isa
可讓對象和類關聯,這一點很好理解,但是爲何在類結構裏面還會有 isa
呢?看過上一篇文章的同窗確定知道這個問題的答案了。沒錯,就是元類。咱們的對象和類關聯起來須要 isa
,一樣的,類和元類之間關聯也須要 isa
。
superclass
指針顧名思義,superclass
指針代表當前類指向的是哪一個父類。通常來講,類的根父類基本上都是 NSObject
類。根元類的父類也是 NSObject
類。
cache
緩存cache
的數據結構爲 cache_t
,其定義以下:
struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; ...省略代碼... }
類的緩存裏面存放的是什麼呢?是屬性?是實例變量?仍是方法?咱們能夠經過閱讀 objc-cache.mm
源文件來解答這個問題。
- objc-cache.m
- Method cache management
- Cache flushing
- Cache garbage collection
- Cache instrumentation
- Dedicated allocator for large caches
上面是 objc-cache.mm
源文件的註釋信息,咱們能夠看到 Method cache management
的出現,翻譯過來就是方法緩存管理。那麼是否是就是說 cache
屬性就是緩存的方法呢?而 OC
中的方法咱們如今尚未進行探索,先假設咱們已經掌握了相關的底層原理,這裏先簡單提一下。
咱們在類裏面編寫的方法,在底層實際上是以SEL
+IMP
的形式存在。SEL
就是方法的選擇器,而IMP
則是具體的方法實現。這裏能夠以書籍的目錄以及內容來類比,咱們查找一篇文章的時候,須要先知道其標題(SEL
),而後在目錄中看有沒有對應的標題,若是有那麼就翻到對應的頁,最後咱們就找到了咱們想要的內容。固然,iOS
中方法要比書籍的例子複雜一些,不過暫時能夠這麼簡單的理解,後面咱們會深刻方法的底層進行探索。
bits
屬性bits
的數據結構類型是 class_data_bits_t
,同時也是一個結構體類型。而咱們閱讀 objc_class
源碼的時候,會發現不少地方都有 bits
的身影,好比:
class_rw_t *data() { return bits.data(); } bool hasCustomRR() { return ! bits.hasDefaultRR(); } bool canAllocFast() { assert(!isFuture()); return bits.canAllocFast(); }
這裏值得咱們注意的是,objc_class
的 data()
方法實際上是返回的 bits
的 data()
方法,而經過這個 data()
方法,咱們發現諸如類的字節對齊、ARC
、元類等特性都有 data()
的出現,這間接說明 bits
屬性實際上是個大容器,有關於內存管理、C++ 析構等內容在其中有定義。
這裏咱們會遇到一個十分重要的知識點: class_rw_t
,data()
方法的返回值就是 class_rw_t
類型的指針對象。咱們在本文後面會重點介紹。
上一節咱們對 OC
中類結構有了基本的瞭解,可是咱們平時最常打交道的內容-屬性,咱們還不知道它到底是存在哪一個地方。接下來咱們要作一件事情,就是在 objc4-756
的源碼中新建一個 Target
,爲何不直接用上面的 macOS
命令行項目呢?由於咱們要開始結合 LLDB
打印一些類的內部信息,因此只能是新建一個依靠於 objc4-756
源碼 project
的 target
出來。一樣的,咱們仍是選擇 macOS
的命令行做爲咱們的 target
。
接着咱們新建一個類 Person
,而後添加一些實例變量和屬性出來。
// Person.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject { NSString *hobby; } @property (nonatomic, copy) NSString *nickName; @end NS_ASSUME_NONNULL_END // main.m #import <Foundation/Foundation.h> #import <objc/runtime.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; Class pClass = object_getClass(p); NSLog(@"%s", p); } return 0; }
咱們打一個斷點到 main.m
文件中的 NSLog
語句處,而後運行剛纔新建的 target
。
target
跑起來以後,咱們在控制檯先打印輸出一下 pClass
的內容:
咱們這個時候須要藉助指針平移來探索,而對於類的內存結構咱們先看下面這張表格:
| 類的內存結構 | 大小(字節) |
| --- | --- |
| isa | 8 | |
| superclass | 8 |
| cache | 16 |
前兩個大小很好理解,由於 isa
和 superclass
都是結構體指針,而在 arm64
環境下,一個結構體指針的內存佔用大小爲 8 字節。而第三個屬性 cache
則須要咱們進行抽絲剝繭了。
cache_t cache; struct cache_t { struct bucket_t *_buckets; // 8 mask_t _mask; // 4 mask_t _occupied; // 4 } typedef uint32_t mask_t;
從上面的代碼咱們能夠看出,cache
屬性實際上是 cache_t
類型的結構體,其內部有一個 8 字節的結構體指針,有 2 個各爲 4 字節的 mask_t
。因此加起來就是 16 個字節。也就是說前三個屬性總共的內存偏移量爲 8 + 8 + 16 = 32 個字節,32 是 10 進制的表示,在 16 進制下就是 20。
bits
屬性咱們剛纔在控制檯打印輸出了 pClass
類對象的內容,咱們簡單畫個圖以下所示:
那麼,類的 bits
屬性的內存地址瓜熟蒂落的就是在 isa
的初始偏移量地址處進行 16 進制下的 20 遞增。也就是
0x1000021c8 + 0x20 = 0x1000021e8
咱們嘗試打印這個地址,注意這裏須要強轉一下:
這裏報錯了,問題實際上是出在咱們的 target
沒有關聯上 libobjc.A.dylib
這個動態庫,咱們關聯上從新運行項目
咱們重複一遍上面的流程:
這一次成功了。在 objc_class
源碼中有:
class_rw_t *data() { return bits.data(); }
咱們不妨打印一下里面的內容:
返回了一個 class_rw_t
指針對象。咱們在 objc4-756
源碼中搜索 class_rw_t
:
struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; Class firstSubclass; Class nextSiblingClass; ...省略代碼... }
顯然的,class_rw_t
也是一個結構體類型,其內部有 methods
、properties
、protocols
等咱們十分熟悉的內容。咱們先猜測一下,咱們的屬性應該存放在 class_rw_t
的 properties
裏面。爲了驗證咱們的猜測,咱們接着進行 LLDB
打印:
咱們再接着打印 properties
:
properties
竟然是空的,難道是 bug?其實否則,這裏咱們還漏掉了一個很是重要的屬性 ro
。咱們來到它的定義:
struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; ...隱藏代碼... }
ro
的類型是 class_ro_t
結構體,它包含了 baseMethodList
、baseProtocols
、ivars
、baseProperties
等屬性。咱們剛纔在 class_rw_t
中沒有找到咱們聲明在 Person
類中的實例變量 hobby
和屬性 nickName
,那麼但願就在 class_ro_t
身上了,咱們打印看看它的內容:
根據名稱咱們猜想屬性應該在 baseProperties
裏面,咱們打印看看:
Bingo! 咱們的屬性 nickName
被找到了,那麼咱們的實例變量 hobby
呢?咱們從 $8 的 count 爲 1 能夠得知確定不在 baseProperites
裏面。根據名稱咱們猜想應該是在 ivars
裏面。
哈哈,hobby
實例變量也被咱們找到了,不過這裏的 count
爲何是 2 呢?咱們打印第二個元素看看:
結果爲 _nickName
。這一結果證明了編譯器會幫助咱們給屬性 nickName
生成一個帶下劃線前綴的實例變量 _nickName
。
至此,咱們能夠得出如下結論:
class_ro_t
是在編譯時就已經肯定了的,存儲的是類的成員變量、屬性、方法和協議等內容。
class_rw_t
是能夠在運行時來拓展類的一些屬性、方法和協議等內容。
研究完了類的屬性是怎麼存儲的,咱們再來看看類的方法。
咱們先給咱們的 Person
類增長一個 sayHello
的實例方法和一個 sayHappy
的類方法。
// Person.h - (void)sayHello; + (void)sayHappy; // Person.m - (void)sayHello { NSLog(@"%s", __func__); } + (void)sayHappy { NSLog(@"%s", __func__); }
按照上面的思路,咱們直接讀取 class_ro_t
中的 baseMethodList
的內容:
sayHello
被打印出來了,說明 baseMethodList
就是存儲實例方法的地方。咱們接着打印剩下的內容:
能夠看到 baseMethodList
中除了咱們的實例方法 sayHello
外,還有屬性 nickName
的 getter
和 setter
方法以及一個 C++
析構方法。可是咱們的類方法 sayHappy
並無被打印出來。
咱們上面已經獲得了屬性,實例方法的是怎麼樣存儲,還留下了一個疑問點,就是類方法是怎麼存儲的,接下來咱們用 Runtime
的 API 來實際測試一下。
// main.m void testInstanceMethod_classToMetaclass(Class pClass){ const char *className = class_getName(pClass); Class metaClass = objc_getMetaClass(className); Method method1 = class_getInstanceMethod(pClass, @selector(sayHello)); Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello)); Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy)); Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy)); NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4); NSLog(@"%s",__func__); } int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; Class pClass = object_getClass(p); testInstanceMethod_classToMetaclass(pClass); NSLog(@"%p", p); } return 0; }
運行後打印結果以下:
首先 testInstanceMethod_classToMetaclass
方法測試的是分別從類和元類去獲取實例方法、類方法的結果。由打印結果咱們能夠知道:
sayHello
是實例方法,存儲於類對象的內存中,不存在於元類對象中。而 sayHappy
是類方法,存儲於元類對象的內存中,不存在於類對象中。sayHello
是類對象的實例方法,跟元類不要緊;sayHappy
是元類對象的實例方法,因此存在元類中。咱們再接着測試:
// main.m void testClassMethod_classToMetaclass(Class pClass){ const char *className = class_getName(pClass); Class metaClass = objc_getMetaClass(className); Method method1 = class_getClassMethod(pClass, @selector(sayHello)); Method method2 = class_getClassMethod(metaClass, @selector(sayHello)); Method method3 = class_getClassMethod(pClass, @selector(sayHappy)); Method method4 = class_getClassMethod(metaClass, @selector(sayHappy)); NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4); NSLog(@"%s",__func__); } int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; Class pClass = object_getClass(p); testClassMethod_classToMetaclass(pClass); NSLog(@"%p", p); } return 0; }
運行後打印結果以下:
從結果咱們能夠看出,對於類對象來講,經過 class_getClassMethod
獲取 sayHappy
是有值的,而獲取 sayHello
是沒有值的;對於元類對象來講,經過 class_getClassMethod
獲取 sayHappy
也是有值的,而獲取 sayHello
是沒有值的。這裏第一點很好理解,可是第二點會有點讓人糊塗,不是說類方法在元類中是體現爲對象方法的嗎?怎麼經過 class_getClassMethod
從元類中也能拿到 sayHappy
,咱們進入到 class_getClassMethod
方法內部能夠解開這個疑惑:
Method class_getClassMethod(Class cls, SEL sel) { if (!cls || !sel) return nil; return class_getInstanceMethod(cls->getMeta(), sel); } Class getMeta() { if (isMetaClass()) return (Class)this; else return this->ISA(); }
能夠很清楚的看到,class_getClassMethod
方法底層其實調用的是 class_getInstanceMethod
,而 cls->getMeta()
方法底層的判斷邏輯是若是已是元類就返回,若是不是就返回類的 isa
。這也就解釋了上面的 sayHappy
爲何會出如今最後的打印中了。
除了上面的 LLDB
打印,咱們還能夠經過 isa
的方式來驗證類方法存放在元類中。
具體的過程筆者再也不贅述。
咱們在探索類和元類的時候,對於其建立時機還不是很清楚,這裏咱們先拋出結論:
那麼如何來證實呢,咱們有兩種方式能夠來證實:
LLDB
打印類和元類的指針
MachoView
打開程序二進制可執行文件查看:
LLDB
來打印類和元類的指針,或者 MachOView
查看二進制可執行文件class_ro_t
結構中存儲了編譯時肯定的屬性、成員變量、方法和協議等內容。咱們完成了對 iOS
中類的底層探索,下一章咱們將對類的緩存進行深一步探索,敬請期待~