iOS中OC對象的本質(zhì)詳解(附面試題) - 底層原理總結(jié)

開胃面試題

1.一個NSObject對象占用多少內(nèi)存?
2.一個繼承自NSObject的Person對象,有一個NSString *name,一個int age,這個Person對象占用多少內(nèi)存?
3.對象的isa指針指向哪里?
4.OC的類信息存放在哪里?

看這篇文章之前可以先回答一下這幾個面試題,然后帶著問題耐心看完這篇文章,再來回答一下看看

一、OC對象在內(nèi)存中的結(jié)構(gòu)

1、轉(zhuǎn)換代碼,查看底層

我們平時編寫的OC代碼,底層都是通過C\C++的結(jié)構(gòu)體來實現(xiàn)的,我們在編譯OC代碼的時候,編譯器會先把OC代碼轉(zhuǎn)成C\C++,再轉(zhuǎn)成匯編語言,然后最終轉(zhuǎn)成機器碼。我們在探索OC對象的本質(zhì)時,可以通過終端將我們寫的OC代碼轉(zhuǎn)成C\C++代碼來探索它的底層實現(xiàn)。

OC代碼轉(zhuǎn)化過程

OC代碼如下

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        NSLog(@"Hello, World!");
    }
    return 0;
}

我們在Mac終端中使用命令行將main.m的OC代碼轉(zhuǎn)化為C\C+代碼。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

我們可以看到生成了一個main-arm64.cpp文件,將這個文件拖入Xcode進行查看(記得不要勾選編譯選項,我們只需查看即可,編譯會報錯)。在main-arm64.cpp文件中,有非常多的代碼,搜索NSObject_IMPL(IMPL代表implementation實現(xiàn)),我們來看一下NSObject_IMPL內(nèi)部

struct NSObject_IMPL {
    Class isa;
};
// 查看Class本質(zhì)
typedef struct objc_class *Class;
我們發(fā)現(xiàn)Class其實就是一個指針,對象底層實現(xiàn)其實就是這個樣子。

我們點擊NSObject進入它的里面,發(fā)現(xiàn)NSObject的內(nèi)部實現(xiàn)是這樣的

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end

轉(zhuǎn)化為底層后其實就是一個C語言的結(jié)構(gòu)體

struct NSObject_IMPL {
    Class isa;
};

那么這個結(jié)構(gòu)體占多大的內(nèi)存空間呢,可以看到這個結(jié)構(gòu)體只有一個成員,即isa指針。在64位架構(gòu)中,指針需要8個字節(jié)內(nèi)存空間,那么一個NSObject對象占用的內(nèi)存空間就是8個字節(jié)。

但是,NSObject對象中還有很多的方法,這些方法不占用內(nèi)存空間嗎?答案是這些方法也占用內(nèi)存空間,但是這些方法有專門的地方放它們,所以這些方法占用的內(nèi)存空間不在NSObject對象中。

2、實際需要與實際分配

從上面的分析中,可以知道一個NSObject對象需要8個字節(jié)內(nèi)存空間,我們現(xiàn)在通過兩個函數(shù)來打印一下NSObject對象的內(nèi)存大小。

#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
        NSLog(@"%zd",class_getInstanceSize([NSObject class]));
        NSLog(@"%zd",malloc_size((__bridge const void *)(object)));
    }
    return 0;
}

打印結(jié)果

2019-05-03 15:02:00.154767+0800 Test[16522:802010] 8
2019-05-03 15:02:00.155803+0800 Test[16522:802010] 16
Program ended with exit code: 0

可以看到,一個結(jié)果是8,一個結(jié)果是16。為什么會是這個結(jié)果呢?因為第一個函數(shù),打印的是NSObject對象的實際大小,第二個函數(shù)打印的是系統(tǒng)為NSObject對象實際開辟的,內(nèi)存空間的大小。

在OC中,系統(tǒng)給對象分配內(nèi)存空間,都是按照16個字節(jié)的倍數(shù)進行分配的,不會因為一個對象只需要1個字節(jié)空間,就給分配1個字節(jié)空間,或者只需要4個字節(jié),就只分配4個字節(jié)空間。

3、自定義類的底層實現(xiàn)

我們定義一個繼承自NSObject的Student類,按照前面的步驟同樣生成C\C++代碼,并查找Student_IMPL

OC代碼

@interface Student : NSObject{
    
    @public
    int _no;
    int _age;
}
@end
@implementation Student

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Student *stu = [[Student alloc] init];
        stu -> _no = 4;
        stu -> _age = 5;
        
        NSLog(@"%@",stu);
    }
    return 0;
}
@end

底層代碼

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};

可以看到,Student_IMPL結(jié)構(gòu)體里的第1個成員是NSObject_IMPL的實現(xiàn)。而前面我們已經(jīng)知道了NSObject_IMPL內(nèi)部其實就是Class isa,最終相當于這樣

struct Student_IMPL {
    Class *isa;
    int _no;
    int _age;
};

所以Student_IMPL結(jié)構(gòu)體占用多少內(nèi)存空間,對象就占用多少內(nèi)存空間。Student_IMPL結(jié)構(gòu)體占用的內(nèi)存空間為,isa指針8個字節(jié) + int類型的_no占用的4個字節(jié) + _age的4個字節(jié)共16個字節(jié)空間。

Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;

那么上述代碼實際上在內(nèi)存中的體現(xiàn)為,創(chuàng)建Student對象首先會分配16個字節(jié)空間,存儲3個東西,isa指針8個字節(jié),_no4個字節(jié),_age4個字節(jié)。

Student對象的存儲空間

Student對象的3個成員變量分別有自己的地址,而stu指向結(jié)構(gòu)體第1個成員變量,即isa指針的地址。因此stu的地址為0x100400110,stu對象在內(nèi)存中占用16個字節(jié)的空間。并且經(jīng)過賦值,_no里面存儲著4,_age里面存儲著5。

驗證Student在內(nèi)存中的布局

struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};

@interface Student : NSObject
{
    @public
    int _no;
    int _age;
}
@end

@implementation Student

int main(int argc, const char * argv[]) {
    @autoreleasepool {
            // 強制轉(zhuǎn)化
            struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
            NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age); // 打印出 _no = 4, _age = 5
    }
    return 0;
}

上述代碼將OC對象類型強轉(zhuǎn)成Student_IMPL類型的結(jié)構(gòu)體,也就是把指向OC對象的指針,指向這個結(jié)構(gòu)體。如果Student對象在內(nèi)存中的布局與結(jié)構(gòu)體Student_IMPL在內(nèi)存中的布局相同,那么就可以轉(zhuǎn)化成功,從而驗證之前的分析。說明stu這個對象指向的內(nèi)存確實是一個結(jié)構(gòu)體。

我們再通過打印看一下stu對象的內(nèi)存大小

NSLog(@"%zd",class_getInstanceSize([Student class]));
NSLog(@"%zd",malloc_size((__bridge const void *)(stu)));

stu的內(nèi)存大小打印結(jié)果為

2019-05-03 16:11:46.384189+0800 Test[22695:848804] 16
2019-05-03 16:11:46.384754+0800 Test[22695:848804] 16
Program ended with exit code: 0

可以看到,stu對象的實際內(nèi)存大小是16字節(jié),系統(tǒng)實際分配的內(nèi)存大小也是16字節(jié)。這也驗證了我們前面說的按照16字節(jié)的倍數(shù)分配規(guī)則。

二、OC對象的分類

OC對象主要分為3種:instance對象(實例對象),class對象(類對象),meta-class對象(元類對象)。

1、實例對象就是通過類alloc出來的對象,每次調(diào)用alloc都會產(chǎn)生新的instance對象

代碼

NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
NSLog(@"obj1:%@",obj1);
NSLog(@"obj2:%@",obj2);

代碼執(zhí)行結(jié)果

2019-11-25 14:06:42.507663+0800 demo[17733:127185] obj1:<NSObject: 0x10054f200>
2019-11-25 14:06:42.508522+0800 demo[17733:127185] obj2:<NSObject: 0x10054c4a0>
Program ended with exit code: 0

obj1和obj2都是NSObject類的實例對象,但他們是兩個不同的對象,占據(jù)著兩塊不同的內(nèi)存。

實例對象主要用來調(diào)用對象方法,存儲成員變量的具體值(包括一個特殊的成員變量isa,和其他成員變量)。我們知道,NSObject對象還有很多方法可以調(diào)用,那這些方法在哪里呢?它們占用內(nèi)存空間嗎?類的的方法當然也占用內(nèi)存空間,但這些方法占用的內(nèi)存空間并不在NSObject類的實例對象中,而是在下面介紹的2個對象中。

2、類對象:我們通過類的+class方法或者runtime方法可以得到類對象,每次調(diào)用+class方法或者runtime方法得到的都是同一個類對象,每一個類在內(nèi)存中有且只有一個類對象

代碼

NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
        
// class方法
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];

// runtime方法
Class objClass4 = object_getClass(obj1);
Class objClass5 = object_getClass(obj2);
NSLog(@"%p %p %p %p %p", objClass1, objClass2, objClass3, objClass4, objClass5);

代碼執(zhí)行結(jié)果

2019-11-25 14:12:22.101947+0800 demo[18095:130169] 0x7fff901be118 0x7fff901be118 0x7fff901be118 0x7fff901be118 0x7fff901be118
Program ended with exit code: 0

可以看到,打印的結(jié)果都是一樣的,這說明不管怎么獲取的一個類的類對象,都是同一個。為什么實例對象可以在內(nèi)存中有很多個,而類對象只有一個呢?這要從類對象在內(nèi)存中存儲的信息說起,先來看一下有哪些信息:
1.isa指針
2.superclass指針
3.類的屬性信息(@property),類的成員變量信息(ivar)
4.類的對象方法信息(instance method),類的協(xié)議信息(protocol)
5.其他一些信息...


class對象中存儲的信息

拿成員變量來說,實例對象存儲的是成員變量具體的值(如Person對象的具體age值),類對象存儲是成員變量的類型和名字(int類型,名字是age)。每一個Person對象的具體age值可能是不一樣的,但是每一個Person對象的這個成員變量的類型都是int,名字都age。所以,實例對象可以有多個,而類對象一個就可以了。

3、元類對象跟類對象一樣,在內(nèi)存中有且只有一個元類對象,我們一般使用runtime方法獲取元類對象

其實,元類對象和類對象是一種類型,都是Class類型。他們在內(nèi)存中的結(jié)構(gòu)是一樣的,存儲的信息也可以是一樣的。但是由于實際用途不一樣,所以元類對象實際上存儲的信息和類對象存儲的信息并不一致。主要包括:
1.isa指針
2.superclass指針
3.類的類方法信息(class method)


元類對象中存儲的信息

三、isa指針與superclass指針

1、isa指針

到這里我們知道了對象有實例對象、類對象、元類對象,他們都有isa指針。那么這些isa指針指向哪里,有什么用?

1.1 實例對象的isa指針指向類對象
我們已經(jīng)知道,類的對象方法存在類對象中。當我們使用實例對象調(diào)用方法的時候,實際上是實例對象通過它的isa指針,找到類對象,最后在類對象里面找到方法進行調(diào)用。

1.2 類對象的isa指針指向元類對象
類的類方法存在元類對象中,當我們調(diào)用類方法的時候,實際上是通過類對象的isa指針,找到元類對象,最后在元類對象中找到方法進行調(diào)用。

1.3 元類對象的isa指針指向基類(即NSObject)的元類對象

1.4. 基類(NSObject)的元類的isa指針指向基類(即NSObject)的類對象(這個比較特殊)

isa指針指向

2、superclass指針

superclass指針存在于類對象和元類對象中。那么這些superclass指針指向哪里,有什么用?

superclass指針存在于類對象和元類對象中,實例對象中沒有superclass指針。類對象的superclass指針指向其父類的類對象,元類對象的superclass指針指向其父類的元類對象。當對象調(diào)用其父類的對象方法時,就需要使用superclass指針

創(chuàng)建一個繼承自NSObject的Person類,再創(chuàng)建一個繼承自Person的Student類。當Student對象要調(diào)用Person的對象方法時,就會通過Student對象的isa指針找到Student的類對象,發(fā)現(xiàn)Student類對象中沒有這個方法,就會通過Student的superclass指針去Person的類對象中找這個方法,找到后進行調(diào)用。

superclass指針指向
superclass調(diào)用類方法的過程與調(diào)用對象方法的過程類似,只不過調(diào)用對象方法是去類對象里面去找,調(diào)用類方法是去元類對象里面去找。

對isa指針和superclass指針的指向進行總結(jié),可以的得到這張總結(jié)圖

isa和superclass總結(jié)

isa、superclass總結(jié):

1.instance的isa指向class
2.class的isa指向meta-class
3.meta-class的isa指向基類(NSObject)的class,如果沒有父類,superclass指針為nil
4.class的superclass指向父類的class,如果沒有父類,superclass指針為nil
5.meta-class的superclass指向父類的meta-class,基類的meta-class的superclass指向基類的class
6.instance調(diào)用對象方法的軌跡:instance的isa找class,方法不存在,就通過superclass找父類
7.class調(diào)用類方法的軌跡,class的isa找meta-class,方法不存在,就通過superclass找父類

掌握OC對象的相關(guān)知識和OC類的相關(guān)知識,對于掌握runtime有莫大的幫助

iOS中OC類的本質(zhì) - 底層原理總結(jié)
iOS中的Runtime(附面試題) - 底層原理總結(jié)

文章堅持優(yōu)化更新中...
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內(nèi)容