
我們在前面探索了
iOS
中的對象原理,面向對象程式設計中有一句名言:
萬物皆對象
那麼對象又是從哪來的呢?有過面向對象程式設計基礎的同學肯定都知道是類派生出對象的,那麼今天我們就一起來探索一下類的底層原理吧。
一、 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
,同樣的,類和元類之間關聯也需要
isa
。
2.2 superclass
指針
superclass
顧名思義,
superclass
指針表明目前類指向的是哪個父類。一般來說,類的根父類基本上都是
NSObject
類。根元類的父類也是
NSObject
類。
2.3 cache
緩存
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
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
屬性
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
中類的底層探索,下一章我們将對類的緩存進行深一步探索,敬請期待~