我們在前面探索了 iOS
中的對象原理,面向對象編程中有一句名言:
萬物皆對象
那麼對象又是從哪來的呢?有過面向對象編程基礎的同學肯定都知道是類派生出對象的,那麼今天我們就一起來探索一下類的底層原理吧。
一、iOS
中的類到底是什麼?
我們在日常開發中大多數情況都是從 NSObject
這個基類來派生出我們需要的類。那麼在 OC
底層,我們的類 Class
到底被編譯成什麼樣子了呢?
我們新建一個 macOS
控制檯項目,然後新建一個 Animal
類出來。
// 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
命令:
clang -rewrite-objc main.m -o main.cpp
這個命令是將我們的 main.m
重寫成 main.cpp
,我們打開這個文件搜索 Animal
:
我們發現有多個地方都出現了 Animal
:
// 1
typedef struct objc_object Animal;
// 2
struct Animal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
// 3
objc_getClass("Animal")
我們先全局搜索第一個 typedef struct objc_object
,發現有 843 個結果
我們通過 Command + G
快捷鍵快速翻閱一下,最終在 7626 行找到了 Class
的定義:
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
指針在這裏是隱藏屬性.
2.1 isa
指針
首先是 isa
指針,我們之前已經探索過了,在對象初始化的時候,通過 isa
可以讓對象和類關聯,這一點很好理解,可是爲什麼在類結構裏面還會有 isa
呢?看過上一篇文章的同學肯定知道這個問題的答案了。沒錯,就是元類。我們的對象和類關聯起來需要 isa
,同樣的,類和元類之間關聯也需要 isa
。
2.2 superclass
指針
顧名思義,superclass
指針表明當前類指向的是哪個父類。一般來說,類的根父類基本上都是 NSObject
類。根元類的父類也是 NSObject
類。
2.3 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
中方法要比書籍的例子複雜一些,不過暫時可以這麼簡單的理解,後面我們會深入方法的底層進行探索。
2.4 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
的內容:
3.1 類的內存結構
我們這個時候需要藉助指針平移來探索,而對於類的內存結構我們先看下面這張表格:
類的內存結構 | 大小(字節) |
---|---|
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。
3.2 探索 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
的方式來驗證類方法存放在元類中。
- 通過 isa 在類對象中找到元類
- 打印元類的 baseMethodsList
具體的過程筆者不再贅述。
六、類和元類的創建時機
我們在探索類和元類的時候,對於其創建時機還不是很清楚,這裏我們先拋出結論:
- 類和元類是在編譯期創建的,即在進行 alloc 操作之前,類和元類就已經被編譯器創建出來了。
那麼如何來證明呢,我們有兩種方式可以來證明:
LLDB
打印類和元類的指針
- 編譯項目後,使用
MachoView
打開程序二進制可執行文件查看:
七、總結
- 類和元類創建於編譯時,可以通過
LLDB
來打印類和元類的指針,或者MachOView
查看二進制可執行文件 - 萬物皆對象:類的本質就是對象
- 類在
class_ro_t
結構中存儲了編譯時確定的屬性、成員變量、方法和協議等內容。 - 實例方法存放在類中
- 類方法存放在元類中
我們完成了對 iOS
中類的底層探索,下一章我們將對類的緩存進行深一步探索,敬請期待~