iOS 底層探索系列
我們?cè)谇懊嫣剿髁?iOS
中的對(duì)象原理厌衔,面向?qū)ο缶幊讨杏幸痪涿?
萬物皆對(duì)象
那么對(duì)象又是從哪來的呢获三?有過面向?qū)ο缶幊袒A(chǔ)的同學(xué)肯定都知道是類派生出對(duì)象的,那么今天我們就一起來探索一下類的底層原理吧。
一双藕、iOS
中的類到底是什么?
我們?cè)谌粘i_發(fā)中大多數(shù)情況都是從 NSObject
這個(gè)基類來派生出我們需要的類遂鹊。那么在 OC
底層栋艳,我們的類 Class
到底被編譯成什么樣子了呢飞袋?
我們新建一個(gè) macOS
控制臺(tái)項(xiàng)目戳气,然后新建一個(gè) 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;
}
我們?cè)诮K端執(zhí)行 clang
命令:
clang -rewrite-objc main.m -o main.cpp
這個(gè)命令是將我們的 main.m
重寫成 main.cpp
巧鸭,我們打開這個(gè)文件搜索 Animal
:
我們發(fā)現(xiàn)有多個(gè)地方都出現(xiàn)了 Animal
:
// 1
typedef struct objc_object Animal;
// 2
struct Animal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
// 3
objc_getClass("Animal")
我們先全局搜索第一個(gè) typedef struct objc_object
瓶您,發(fā)現(xiàn)有 843 個(gè)結(jié)果
我們通過 Command + G
快捷鍵快速翻閱一下,最終在 7626 行找到了 Class
的定義:
typedef struct objc_class *Class;
由這行代碼我們可以得出一個(gè)結(jié)論纲仍,Class
類型在底層是一個(gè)結(jié)構(gòu)體類型的指針呀袱,這個(gè)結(jié)構(gòu)體類型為 objc_class
。
再搜索 typedef struct objc_class
發(fā)現(xiàn)搜不出來了郑叠,這個(gè)時(shí)候我們需要在 objc4-756
源碼中進(jìn)行探索了夜赵。
我們?cè)?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();
}
}
看到這里锻拘,細(xì)心的讀者可能會(huì)發(fā)現(xiàn)油吭,我們?cè)谇懊嫣剿鲗?duì)象原理中遇到的 objc_object
再次出現(xiàn)了,并且這次是作為 objc_class
的父類署拟。這里再次引用那句經(jīng)典名言 萬物皆對(duì)象,也就是說類其實(shí)也是一種對(duì)象歌豺。
由此推穷,我們可以簡(jiǎn)單總結(jié)一下類和對(duì)象在 C
和 OC
中分別的定義
C | OC |
---|---|
objc_object | NSObject |
objc_class | NSObject(Class) |
二、類的結(jié)構(gòu)是什么樣的呢类咧?
通過上面的探索馒铃,我們已經(jīng)知道了類本質(zhì)上也是對(duì)象,而日常開發(fā)中常見的成員變量痕惋、屬性区宇、方法、協(xié)議等都是在類里面存在的值戳,那么我們是不是可以猜想在 iOS
底層议谷,類其實(shí)就存儲(chǔ)了這些內(nèi)容呢?
我們可以通過分析源碼來驗(yàn)證我們的猜想堕虹。
從上一節(jié)中 objc_class
的定義處卧晓,我們可以梳理出 Class
中的 4 個(gè)屬性
-
isa
指針 -
superclass
指針 cache
bits
需要值得注意的是,這里的
isa
指針在這里是隱藏屬性.
2.1 isa
指針
首先是 isa
指針赴捞,我們之前已經(jīng)探索過了逼裆,在對(duì)象初始化的時(shí)候,通過 isa
可以讓對(duì)象和類關(guān)聯(lián)赦政,這一點(diǎn)很好理解胜宇,可是為什么在類結(jié)構(gòu)里面還會(huì)有 isa
呢?看過上一篇文章的同學(xué)肯定知道這個(gè)問題的答案了。沒錯(cuò)桐愉,就是元類封寞。我們的對(duì)象和類關(guān)聯(lián)起來需要 isa
,同樣的仅财,類和元類之間關(guān)聯(lián)也需要 isa
狈究。
2.2 superclass
指針
顧名思義,superclass
指針表明當(dāng)前類指向的是哪個(gè)父類盏求。一般來說抖锥,類的根父類基本上都是 NSObject
類。根元類的父類也是 NSObject
類碎罚。
2.3 cache
緩存
cache
的數(shù)據(jù)結(jié)構(gòu)為 cache_t
磅废,其定義如下:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
...省略代碼...
}
類的緩存里面存放的是什么呢?是屬性荆烈?是實(shí)例變量拯勉?還是方法?我們可以通過閱讀 objc-cache.mm
源文件來解答這個(gè)問題憔购。
- objc-cache.m
- Method cache management
- Cache flushing
- Cache garbage collection
- Cache instrumentation
- Dedicated allocator for large caches
上面是 objc-cache.mm
源文件的注釋信息宫峦,我們可以看到 Method cache management
的出現(xiàn),翻譯過來就是方法緩存管理玫鸟。那么是不是就是說 cache
屬性就是緩存的方法呢导绷?而 OC
中的方法我們現(xiàn)在還沒有進(jìn)行探索,先假設(shè)我們已經(jīng)掌握了相關(guān)的底層原理屎飘,這里先簡(jiǎn)單提一下妥曲。
我們?cè)陬惱锩婢帉懙姆椒ǎ诘讓悠鋵?shí)是以
SEL
+IMP
的形式存在钦购。SEL
就是方法的選擇器檐盟,而IMP
則是具體的方法實(shí)現(xiàn)。這里可以以書籍的目錄以及內(nèi)容來類比押桃,我們查找一篇文章的時(shí)候葵萎,需要先知道其標(biāo)題(SEL
),然后在目錄中看有沒有對(duì)應(yīng)的標(biāo)題怨规,如果有那么就翻到對(duì)應(yīng)的頁陌宿,最后我們就找到了我們想要的內(nèi)容。當(dāng)然波丰,iOS
中方法要比書籍的例子復(fù)雜一些壳坪,不過暫時(shí)可以這么簡(jiǎn)單的理解,后面我們會(huì)深入方法的底層進(jìn)行探索掰烟。
2.4 bits
屬性
bits
的數(shù)據(jù)結(jié)構(gòu)類型是 class_data_bits_t
爽蝴,同時(shí)也是一個(gè)結(jié)構(gòu)體類型沐批。而我們閱讀 objc_class
源碼的時(shí)候,會(huì)發(fā)現(xiàn)很多地方都有 bits
的身影蝎亚,比如:
class_rw_t *data() {
return bits.data();
}
bool hasCustomRR() {
return ! bits.hasDefaultRR();
}
bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
這里值得我們注意的是九孩,objc_class
的 data()
方法其實(shí)是返回的 bits
的 data()
方法,而通過這個(gè) data()
方法发框,我們發(fā)現(xiàn)諸如類的字節(jié)對(duì)齊躺彬、ARC
、元類等特性都有 data()
的出現(xiàn)梅惯,這間接說明 bits
屬性其實(shí)是個(gè)大容器宪拥,有關(guān)于內(nèi)存管理、C++ 析構(gòu)等內(nèi)容在其中有定義铣减。
這里我們會(huì)遇到一個(gè)十分重要的知識(shí)點(diǎn): class_rw_t
她君,data()
方法的返回值就是 class_rw_t
類型的指針對(duì)象。我們?cè)诒疚暮竺鏁?huì)重點(diǎn)介紹葫哗。
三缔刹、類的屬性存在哪?
上一節(jié)我們對(duì) OC
中類結(jié)構(gòu)有了基本的了解劣针,但是我們平時(shí)最常打交道的內(nèi)容-屬性校镐,我們還不知道它究竟是存在哪個(gè)地方。接下來我們要做一件事情酿秸,就是在 objc4-756
的源碼中新建一個(gè) Target
灭翔,為什么不直接用上面的 macOS
命令行項(xiàng)目呢?因?yàn)槲覀円_始結(jié)合 LLDB
打印一些類的內(nèi)部信息辣苏,所以只能是新建一個(gè)依靠于 objc4-756
源碼 project
的 target
出來。同樣的哄褒,我們還是選擇 macOS
的命令行作為我們的 target
稀蟋。
接著我們新建一個(gè)類 Person
,然后添加一些實(shí)例變量和屬性出來呐赡。
// 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;
}
我們打一個(gè)斷點(diǎn)到 main.m
文件中的 NSLog
語句處退客,然后運(yùn)行剛才新建的 target
。
target
跑起來之后链嘀,我們?cè)诳刂婆_(tái)先打印輸出一下 pClass
的內(nèi)容:
3.1 類的內(nèi)存結(jié)構(gòu)
我們這個(gè)時(shí)候需要借助指針平移來探索萌狂,而對(duì)于類的內(nèi)存結(jié)構(gòu)我們先看下面這張表格:
類的內(nèi)存結(jié)構(gòu) | 大小(字節(jié)) | |
---|---|---|
isa | 8 | |
superclass | 8 | |
cache | 16 |
前兩個(gè)大小很好理解,因?yàn)?isa
和 superclass
都是結(jié)構(gòu)體指針怀泊,而在 arm64
環(huán)境下茫藏,一個(gè)結(jié)構(gòu)體指針的內(nèi)存占用大小為 8 字節(jié)。而第三個(gè)屬性 cache
則需要我們進(jìn)行抽絲剝繭了霹琼。
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
屬性其實(shí)是 cache_t
類型的結(jié)構(gòu)體凉当,其內(nèi)部有一個(gè) 8 字節(jié)的結(jié)構(gòu)體指針,有 2 個(gè)各為 4 字節(jié)的 mask_t
售葡。所以加起來就是 16 個(gè)字節(jié)看杭。也就是說前三個(gè)屬性總共的內(nèi)存偏移量為 8 + 8 + 16 = 32 個(gè)字節(jié),32 是 10 進(jìn)制的表示挟伙,在 16 進(jìn)制下就是 20楼雹。
3.2 探索 bits
屬性
我們剛才在控制臺(tái)打印輸出了 pClass
類對(duì)象的內(nèi)容,我們簡(jiǎn)單畫個(gè)圖如下所示:
那么尖阔,類的 bits
屬性的內(nèi)存地址順理成章的就是在 isa
的初始偏移量地址處進(jìn)行 16 進(jìn)制下的 20 遞增贮缅。也就是
0x1000021c8 + 0x20 = 0x1000021e8
我們嘗試打印這個(gè)地址,注意這里需要強(qiáng)轉(zhuǎn)一下:
這里報(bào)錯(cuò)了诺祸,問題其實(shí)是出在我們的 target
沒有關(guān)聯(lián)上 libobjc.A.dylib
這個(gè)動(dòng)態(tài)庫携悯,我們關(guān)聯(lián)上重新運(yùn)行項(xiàng)目
我們重復(fù)一遍上面的流程:
這一次成功了。在 objc_class
源碼中有:
class_rw_t *data() {
return bits.data();
}
我們不妨打印一下里面的內(nèi)容:
返回了一個(gè) class_rw_t
指針對(duì)象筷笨。我們?cè)?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
也是一個(gè)結(jié)構(gòu)體類型,其內(nèi)部有 methods
胃夏、properties
轴或、protocols
等我們十分熟悉的內(nèi)容。我們先猜想一下仰禀,我們的屬性應(yīng)該存放在 class_rw_t
的 properties
里面照雁。為了驗(yàn)證我們的猜想,我們接著進(jìn)行 LLDB
打印:
我們?cè)俳又蛴?properties
:
properties
居然是空的答恶,難道是 bug?其實(shí)不然饺蚊,這里我們還漏掉了一個(gè)非常重要的屬性 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
結(jié)構(gòu)體悬嗓,它包含了 baseMethodList
污呼、baseProtocols
、ivars
包竹、baseProperties
等屬性燕酷。我們剛才在 class_rw_t
中沒有找到我們聲明在 Person
類中的實(shí)例變量 hobby
和屬性 nickName
,那么希望就在 class_ro_t
身上了周瞎,我們打印看看它的內(nèi)容:
根據(jù)名稱我們猜測(cè)屬性應(yīng)該在 baseProperties
里面苗缩,我們打印看看:
Bingo! 我們的屬性 nickName
被找到了,那么我們的實(shí)例變量 hobby
呢声诸?我們從 $8 的 count 為 1 可以得知肯定不在 baseProperites
里面。根據(jù)名稱我們猜測(cè)應(yīng)該是在 ivars
里面双絮。
哈哈浴麻,hobby
實(shí)例變量也被我們找到了得问,不過這里的 count
為什么是 2 呢?我們打印第二個(gè)元素看看:
結(jié)果為 _nickName
软免。這一結(jié)果證實(shí)了編譯器會(huì)幫助我們給屬性 nickName
生成一個(gè)帶下劃線前綴的實(shí)例變量 _nickName
宫纬。
至此,我們可以得出以下結(jié)論:
class_ro_t
是在編譯時(shí)就已經(jīng)確定了的膏萧,存儲(chǔ)的是類的成員變量漓骚、屬性、方法和協(xié)議等內(nèi)容榛泛。
class_rw_t
是可以在運(yùn)行時(shí)來拓展類的一些屬性蝌蹂、方法和協(xié)議等內(nèi)容。
四曹锨、類的方法存在哪孤个?
研究完了類的屬性是怎么存儲(chǔ)的,我們?cè)賮砜纯搭惖姆椒ā?/p>
我們先給我們的 Person
類增加一個(gè) sayHello
的實(shí)例方法和一個(gè) sayHappy
的類方法沛简。
// Person.h
- (void)sayHello;
+ (void)sayHappy;
// Person.m
- (void)sayHello
{
NSLog(@"%s", __func__);
}
+ (void)sayHappy
{
NSLog(@"%s", __func__);
}
按照上面的思路齐鲤,我們直接讀取 class_ro_t
中的 baseMethodList
的內(nèi)容:
sayHello
被打印出來了,說明 baseMethodList
就是存儲(chǔ)實(shí)例方法的地方椒楣。我們接著打印剩下的內(nèi)容:
可以看到 baseMethodList
中除了我們的實(shí)例方法 sayHello
外给郊,還有屬性 nickName
的 getter
和 setter
方法以及一個(gè) C++
析構(gòu)方法。但是我們的類方法 sayHappy
并沒有被打印出來捧灰。
五淆九、類的類方法存在哪?
我們上面已經(jīng)得到了屬性毛俏,實(shí)例方法的是怎么樣存儲(chǔ)炭庙,還留下了一個(gè)疑問點(diǎn),就是類方法是怎么存儲(chǔ)的煌寇,接下來我們用 Runtime
的 API 來實(shí)際測(cè)試一下煤搜。
// 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;
}
運(yùn)行后打印結(jié)果如下:
首先 testInstanceMethod_classToMetaclass
方法測(cè)試的是分別從類和元類去獲取實(shí)例方法、類方法的結(jié)果唧席。由打印結(jié)果我們可以知道:
- 對(duì)于類對(duì)象來說,
sayHello
是實(shí)例方法嘲驾,存儲(chǔ)于類對(duì)象的內(nèi)存中淌哟,不存在于元類對(duì)象中。而sayHappy
是類方法辽故,存儲(chǔ)于元類對(duì)象的內(nèi)存中徒仓,不存在于類對(duì)象中。 - 對(duì)于元類對(duì)象來說誊垢,
sayHello
是類對(duì)象的實(shí)例方法掉弛,跟元類沒關(guān)系症见;sayHappy
是元類對(duì)象的實(shí)例方法,所以存在元類中殃饿。
我們?cè)俳又鴾y(cè)試:
// 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;
}
運(yùn)行后打印結(jié)果如下:
從結(jié)果我們可以看出谋作,對(duì)于類對(duì)象來說,通過 class_getClassMethod
獲取 sayHappy
是有值的乎芳,而獲取 sayHello
是沒有值的遵蚜;對(duì)于元類對(duì)象來說,通過 class_getClassMethod
獲取 sayHappy
也是有值的奈惑,而獲取 sayHello
是沒有值的吭净。這里第一點(diǎn)很好理解,但是第二點(diǎn)會(huì)有點(diǎn)讓人糊涂肴甸,不是說類方法在元類中是體現(xiàn)為對(duì)象方法的嗎寂殉?怎么通過 class_getClassMethod
從元類中也能拿到 sayHappy
,我們進(jìn)入到 class_getClassMethod
方法內(nèi)部可以解開這個(gè)疑惑:
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
方法底層其實(shí)調(diào)用的是 class_getInstanceMethod
友扰,而 cls->getMeta()
方法底層的判斷邏輯是如果已經(jīng)是元類就返回,如果不是就返回類的 isa
晤斩。這也就解釋了上面的 sayHappy
為什么會(huì)出現(xiàn)在最后的打印中了焕檬。
除了上面的 LLDB
打印,我們還可以通過 isa
的方式來驗(yàn)證類方法存放在元類中澳泵。
- 通過 isa 在類對(duì)象中找到元類
- 打印元類的 baseMethodsList
具體的過程筆者不再贅述实愚。
六、類和元類的創(chuàng)建時(shí)機(jī)
我們?cè)谔剿黝惡驮惖臅r(shí)候兔辅,對(duì)于其創(chuàng)建時(shí)機(jī)還不是很清楚腊敲,這里我們先拋出結(jié)論:
- 類和元類是在編譯期創(chuàng)建的,即在進(jìn)行 alloc 操作之前维苔,類和元類就已經(jīng)被編譯器創(chuàng)建出來了碰辅。
那么如何來證明呢,我們有兩種方式可以來證明:
-
LLDB
打印類和元類的指針
- 編譯項(xiàng)目后介时,使用
MachoView
打開程序二進(jìn)制可執(zhí)行文件查看:
六没宾、總結(jié)
- 類和元類創(chuàng)建于編譯時(shí),可以通過
LLDB
來打印類和元類的指針沸柔,或者MachOView
查看二進(jìn)制可執(zhí)行文件 - 萬物皆對(duì)象:類的本質(zhì)就是對(duì)象
- 類在
class_ro_t
結(jié)構(gòu)中存儲(chǔ)了編譯時(shí)確定的屬性循衰、成員變量、方法和協(xié)議等內(nèi)容褐澎。 - 實(shí)例方法存放在類中
- 類方法存放在元類中
我們完成了對(duì) iOS
中類的底層探索会钝,下一章我們將對(duì)類的緩存進(jìn)行深一步探索,敬請(qǐng)期待~