開胃面試題
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代碼如下
#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é),_no
4個字節(jié),_age
4個字節(jié)。
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.其他一些信息...
拿成員變量來說,實例對象存儲的是成員變量具體的值(如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)的類對象(這個比較特殊)
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調(diào)用類方法的過程與調(diào)用對象方法的過程類似,只不過調(diào)用對象方法是去類對象里面去找,調(diào)用類方法是去元類對象里面去找。
對isa指針和superclass指針的指向進行總結(jié),可以的得到這張總結(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é)