
iOS 底層探索系列
我們?cè)谇懊嫣剿髁?iOS 中的對(duì)象原理,面向?qū)ο缶幊讨杏幸痪涿?
萬(wàn)物皆對(duì)象
那么對(duì)象又是從哪來(lái)的呢?有過(guò)面向?qū)ο缶幊袒A(chǔ)的同學(xué)肯定都知道是類(lèi)派生出對(duì)象的,那么今天我們就一起來(lái)探索一下類(lèi)的底層原理吧。
一、iOS 中的類(lèi)到底是什么?
我們?cè)谌粘i_(kāi)發(fā)中大多數(shù)情況都是從 NSObject 這個(gè)基類(lèi)來(lái)派生出我們需要的類(lèi)。那么在 OC 底層,我們的類(lèi) Class 到底被編譯成什么樣子了呢?
我們新建一個(gè) macOS 控制臺(tái)項(xiàng)目,然后新建一個(gè) Animal 類(lèi)出來(lái)。
// 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 重寫(xiě)成 main.cpp,我們打開(kāi)這個(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é)果
我們通過(guò) Command + G 快捷鍵快速翻閱一下,最終在 7626 行找到了 Class 的定義:
typedef struct objc_class *Class;
由這行代碼我們可以得出一個(gè)結(jié)論,Class 類(lèi)型在底層是一個(gè)結(jié)構(gòu)體類(lèi)型的指針,這個(gè)結(jié)構(gòu)體類(lèi)型為 objc_class。
再搜索 typedef struct objc_class 發(fā)現(xiàn)搜不出來(lái)了,這個(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 的父類(lèi)。這里再次引用那句經(jīng)典名言 萬(wàn)物皆對(duì)象,也就是說(shuō)類(lèi)其實(shí)也是一種對(duì)象。
由此,我們可以簡(jiǎn)單總結(jié)一下類(lèi)和對(duì)象在 C 和 OC 中分別的定義
| C | OC |
|---|---|
| objc_object | NSObject |
| objc_class | NSObject(Class) |
二、類(lèi)的結(jié)構(gòu)是什么樣的呢?
通過(guò)上面的探索,我們已經(jīng)知道了類(lèi)本質(zhì)上也是對(duì)象,而日常開(kāi)發(fā)中常見(jiàn)的成員變量、屬性、方法、協(xié)議等都是在類(lèi)里面存在的,那么我們是不是可以猜想在 iOS 底層,類(lèi)其實(shí)就存儲(chǔ)了這些內(nèi)容呢?
我們可以通過(guò)分析源碼來(lái)驗(yàn)證我們的猜想。
從上一節(jié)中 objc_class 的定義處,我們可以梳理出 Class 中的 4 個(gè)屬性
-
isa指針 -
superclass指針 cachebits
需要值得注意的是,這里的
isa指針在這里是隱藏屬性.
2.1 isa 指針
首先是 isa 指針,我們之前已經(jīng)探索過(guò)了,在對(duì)象初始化的時(shí)候,通過(guò) isa 可以讓對(duì)象和類(lèi)關(guān)聯(lián),這一點(diǎn)很好理解,可是為什么在類(lèi)結(jié)構(gòu)里面還會(huì)有 isa 呢?看過(guò)上一篇文章的同學(xué)肯定知道這個(gè)問(wèn)題的答案了。沒(méi)錯(cuò),就是元類(lèi)。我們的對(duì)象和類(lèi)關(guān)聯(lián)起來(lái)需要 isa,同樣的,類(lèi)和元類(lèi)之間關(guān)聯(lián)也需要 isa。
2.2 superclass 指針
顧名思義,superclass 指針表明當(dāng)前類(lèi)指向的是哪個(gè)父類(lèi)。一般來(lái)說(shuō),類(lèi)的根父類(lèi)基本上都是 NSObject 類(lèi)。根元類(lèi)的父類(lèi)也是 NSObject 類(lèi)。
2.3 cache 緩存
cache 的數(shù)據(jù)結(jié)構(gòu)為 cache_t,其定義如下:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
...省略代碼...
}
類(lèi)的緩存里面存放的是什么呢?是屬性?是實(shí)例變量?還是方法?我們可以通過(guò)閱讀 objc-cache.mm 源文件來(lái)解答這個(gè)問(wèn)題。
- 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),翻譯過(guò)來(lái)就是方法緩存管理。那么是不是就是說(shuō) cache 屬性就是緩存的方法呢?而 OC 中的方法我們現(xiàn)在還沒(méi)有進(jìn)行探索,先假設(shè)我們已經(jīng)掌握了相關(guān)的底層原理,這里先簡(jiǎn)單提一下。
我們?cè)陬?lèi)里面編寫(xiě)的方法,在底層其實(shí)是以
SEL+IMP的形式存在。SEL就是方法的選擇器,而IMP則是具體的方法實(shí)現(xiàn)。這里可以以書(shū)籍的目錄以及內(nèi)容來(lái)類(lèi)比,我們查找一篇文章的時(shí)候,需要先知道其標(biāo)題(SEL),然后在目錄中看有沒(méi)有對(duì)應(yīng)的標(biāo)題,如果有那么就翻到對(duì)應(yīng)的頁(yè),最后我們就找到了我們想要的內(nèi)容。當(dāng)然,iOS中方法要比書(shū)籍的例子復(fù)雜一些,不過(guò)暫時(shí)可以這么簡(jiǎn)單的理解,后面我們會(huì)深入方法的底層進(jìn)行探索。
2.4 bits 屬性
bits 的數(shù)據(jù)結(jié)構(gòu)類(lèi)型是 class_data_bits_t,同時(shí)也是一個(gè)結(jié)構(gòu)體類(lèi)型。而我們閱讀 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() 方法,而通過(guò)這個(gè) data() 方法,我們發(fā)現(xiàn)諸如類(lèi)的字節(jié)對(duì)齊、ARC、元類(lèi)等特性都有 data() 的出現(xiàn),這間接說(shuō)明 bits 屬性其實(shí)是個(gè)大容器,有關(guān)于內(nèi)存管理、C++ 析構(gòu)等內(nèi)容在其中有定義。
這里我們會(huì)遇到一個(gè)十分重要的知識(shí)點(diǎn): class_rw_t,data() 方法的返回值就是 class_rw_t 類(lèi)型的指針對(duì)象。我們?cè)诒疚暮竺鏁?huì)重點(diǎn)介紹。
三、類(lèi)的屬性存在哪?
上一節(jié)我們對(duì) OC 中類(lèi)結(jié)構(gòu)有了基本的了解,但是我們平時(shí)最常打交道的內(nèi)容-屬性,我們還不知道它究竟是存在哪個(gè)地方。接下來(lái)我們要做一件事情,就是在 objc4-756 的源碼中新建一個(gè) Target,為什么不直接用上面的 macOS 命令行項(xiàng)目呢?因?yàn)槲覀円_(kāi)始結(jié)合 LLDB 打印一些類(lèi)的內(nèi)部信息,所以只能是新建一個(gè)依靠于 objc4-756 源碼 project 的 target 出來(lái)。同樣的,我們還是選擇 macOS 的命令行作為我們的 target。
接著我們新建一個(gè)類(lèi) Person,然后添加一些實(shí)例變量和屬性出來(lái)。
// 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ǔ)句處,然后運(yùn)行剛才新建的 target。
target 跑起來(lái)之后,我們?cè)诳刂婆_(tái)先打印輸出一下 pClass 的內(nèi)容:
3.1 類(lèi)的內(nèi)存結(jié)構(gòu)
我們這個(gè)時(shí)候需要借助指針平移來(lái)探索,而對(duì)于類(lèi)的內(nèi)存結(jié)構(gòu)我們先看下面這張表格:
| 類(lèi)的內(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 類(lèi)型的結(jié)構(gòu)體,其內(nèi)部有一個(gè) 8 字節(jié)的結(jié)構(gòu)體指針,有 2 個(gè)各為 4 字節(jié)的 mask_t。所以加起來(lái)就是 16 個(gè)字節(jié)。也就是說(shuō)前三個(gè)屬性總共的內(nèi)存偏移量為 8 + 8 + 16 = 32 個(gè)字節(jié),32 是 10 進(jìn)制的表示,在 16 進(jìn)制下就是 20。
3.2 探索 bits 屬性
我們剛才在控制臺(tái)打印輸出了 pClass 類(lèi)對(duì)象的內(nèi)容,我們簡(jiǎn)單畫(huà)個(gè)圖如下所示:
那么,類(lèi)的 bits 屬性的內(nèi)存地址順理成章的就是在 isa 的初始偏移量地址處進(jìn)行 16 進(jìn)制下的 20 遞增。也就是
0x1000021c8 + 0x20 = 0x1000021e8
我們嘗試打印這個(gè)地址,注意這里需要強(qiáng)轉(zhuǎn)一下:
這里報(bào)錯(cuò)了,問(wèn)題其實(shí)是出在我們的 target 沒(méi)有關(guān)聯(lián)上 libobjc.A.dylib 這個(gè)動(dòng)態(tài)庫(kù),我們關(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)體類(lèi)型,其內(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。我們來(lái)到它的定義:
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 的類(lèi)型是 class_ro_t 結(jié)構(gòu)體,它包含了 baseMethodList、baseProtocols、ivars、baseProperties 等屬性。我們剛才在 class_rw_t 中沒(méi)有找到我們聲明在 Person 類(lèi)中的實(shí)例變量 hobby 和屬性 nickName,那么希望就在 class_ro_t 身上了,我們打印看看它的內(nèi)容:
根據(jù)名稱(chēng)我們猜測(cè)屬性應(yīng)該在 baseProperties 里面,我們打印看看:
Bingo! 我們的屬性 nickName 被找到了,那么我們的實(shí)例變量 hobby 呢?我們從 $8 的 count 為 1 可以得知肯定不在 baseProperites 里面。根據(jù)名稱(chēng)我們猜測(cè)應(yīng)該是在 ivars 里面。
哈哈,hobby 實(shí)例變量也被我們找到了,不過(guò)這里的 count 為什么是 2 呢?我們打印第二個(gè)元素看看:
結(jié)果為 _nickName。這一結(jié)果證實(shí)了編譯器會(huì)幫助我們給屬性 nickName 生成一個(gè)帶下劃線前綴的實(shí)例變量 _nickName。
至此,我們可以得出以下結(jié)論:
class_ro_t是在編譯時(shí)就已經(jīng)確定了的,存儲(chǔ)的是類(lèi)的成員變量、屬性、方法和協(xié)議等內(nèi)容。
class_rw_t是可以在運(yùn)行時(shí)來(lái)拓展類(lèi)的一些屬性、方法和協(xié)議等內(nèi)容。
四、類(lèi)的方法存在哪?
研究完了類(lèi)的屬性是怎么存儲(chǔ)的,我們?cè)賮?lái)看看類(lèi)的方法。
我們先給我們的 Person 類(lèi)增加一個(gè) sayHello 的實(shí)例方法和一個(gè) sayHappy 的類(lèi)方法。
// 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 被打印出來(lái)了,說(shuō)明 baseMethodList 就是存儲(chǔ)實(shí)例方法的地方。我們接著打印剩下的內(nèi)容:
可以看到 baseMethodList 中除了我們的實(shí)例方法 sayHello 外,還有屬性 nickName 的 getter 和 setter 方法以及一個(gè) C++ 析構(gòu)方法。但是我們的類(lèi)方法 sayHappy 并沒(méi)有被打印出來(lái)。
五、類(lèi)的類(lèi)方法存在哪?
我們上面已經(jīng)得到了屬性,實(shí)例方法的是怎么樣存儲(chǔ),還留下了一個(gè)疑問(wèn)點(diǎn),就是類(lèi)方法是怎么存儲(chǔ)的,接下來(lái)我們用 Runtime 的 API 來(lái)實(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è)試的是分別從類(lèi)和元類(lèi)去獲取實(shí)例方法、類(lèi)方法的結(jié)果。由打印結(jié)果我們可以知道:
- 對(duì)于類(lèi)對(duì)象來(lái)說(shuō),
sayHello是實(shí)例方法,存儲(chǔ)于類(lèi)對(duì)象的內(nèi)存中,不存在于元類(lèi)對(duì)象中。而sayHappy是類(lèi)方法,存儲(chǔ)于元類(lèi)對(duì)象的內(nèi)存中,不存在于類(lèi)對(duì)象中。 - 對(duì)于元類(lèi)對(duì)象來(lái)說(shuō),
sayHello是類(lèi)對(duì)象的實(shí)例方法,跟元類(lèi)沒(méi)關(guān)系;sayHappy是元類(lèi)對(duì)象的實(shí)例方法,所以存在元類(lèi)中。
我們?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ì)于類(lèi)對(duì)象來(lái)說(shuō),通過(guò) class_getClassMethod 獲取 sayHappy 是有值的,而獲取 sayHello 是沒(méi)有值的;對(duì)于元類(lèi)對(duì)象來(lái)說(shuō),通過(guò) class_getClassMethod 獲取 sayHappy 也是有值的,而獲取 sayHello 是沒(méi)有值的。這里第一點(diǎn)很好理解,但是第二點(diǎn)會(huì)有點(diǎn)讓人糊涂,不是說(shuō)類(lèi)方法在元類(lèi)中是體現(xiàn)為對(duì)象方法的嗎?怎么通過(guò) class_getClassMethod 從元類(lèi)中也能拿到 sayHappy,我們進(jìn)入到 class_getClassMethod 方法內(nèi)部可以解開(kā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)是元類(lèi)就返回,如果不是就返回類(lèi)的 isa。這也就解釋了上面的 sayHappy 為什么會(huì)出現(xiàn)在最后的打印中了。
除了上面的 LLDB 打印,我們還可以通過(guò) isa 的方式來(lái)驗(yàn)證類(lèi)方法存放在元類(lèi)中。
- 通過(guò) isa 在類(lèi)對(duì)象中找到元類(lèi)
- 打印元類(lèi)的 baseMethodsList
具體的過(guò)程筆者不再贅述。
六、類(lèi)和元類(lèi)的創(chuàng)建時(shí)機(jī)
我們?cè)谔剿黝?lèi)和元類(lèi)的時(shí)候,對(duì)于其創(chuàng)建時(shí)機(jī)還不是很清楚,這里我們先拋出結(jié)論:
- 類(lèi)和元類(lèi)是在編譯期創(chuàng)建的,即在進(jìn)行 alloc 操作之前,類(lèi)和元類(lèi)就已經(jīng)被編譯器創(chuàng)建出來(lái)了。
那么如何來(lái)證明呢,我們有兩種方式可以來(lái)證明:
-
LLDB打印類(lèi)和元類(lèi)的指針
- 編譯項(xiàng)目后,使用
MachoView打開(kāi)程序二進(jìn)制可執(zhí)行文件查看:
六、總結(jié)
- 類(lèi)和元類(lèi)創(chuàng)建于編譯時(shí),可以通過(guò)
LLDB來(lái)打印類(lèi)和元類(lèi)的指針,或者MachOView查看二進(jìn)制可執(zhí)行文件 - 萬(wàn)物皆對(duì)象:類(lèi)的本質(zhì)就是對(duì)象
- 類(lèi)在
class_ro_t結(jié)構(gòu)中存儲(chǔ)了編譯時(shí)確定的屬性、成員變量、方法和協(xié)議等內(nèi)容。 - 實(shí)例方法存放在類(lèi)中
- 類(lèi)方法存放在元類(lèi)中
我們完成了對(duì) iOS 中類(lèi)的底層探索,下一章我們將對(duì)類(lèi)的緩存進(jìn)行深一步探索,敬請(qǐng)期待~