面試問題:
- 一個NSObject對象占用多少內(nèi)存?
- 對象的isa指針指向哪里匕积?
- OC的類信息存放在哪里盈罐?
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
return 0;
}
}
第一個問題實質(zhì)上就可以轉(zhuǎn)化為objc這個指針指向的內(nèi)存區(qū)域有多大。為了搞清這個問題闪唆,我們就要搞清楚NSObject在內(nèi)存中是怎么布局的盅粪,它的底層原理。
Objective-c的本質(zhì)
我們平時編寫的objective-c代碼悄蕾,底層實現(xiàn)其實都是C/C++代碼
Objective-C > C/C++ > 匯編語言 > 機器語言
所以O(shè)bjective-C的面向?qū)ο蠖际腔贑/C++的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的奠骄。
Objective-C的對象含鳞,類主要是基于C/C++的結(jié)構(gòu)體來實現(xiàn)的蝉绷。
- 將Objective-c代碼轉(zhuǎn)化為C/C++的代碼:
1>在命令行cd到放Objective-c代碼的文件夾
2>比如我們要把文件夾中的main.m文件轉(zhuǎn)化熔吗,我們可以再命令行輸入:clang -rewrite-objc main.m,然后在這個文件夾下我們就得到了轉(zhuǎn)化成功的文件main.cpp桅狠。
我們將上面的代碼轉(zhuǎn)化為C++的源碼中跌,得到main.cpp晒他。
在7000多行我們找到這樣一個結(jié)構(gòu)體:
//NSObject implemention
struct NSObject_IMPL {
Class isa;
};
這個結(jié)構(gòu)體就是NSObject對象在內(nèi)存中的本質(zhì)逸贾。
另外铝侵,我們按住command點擊進NSObject里面看一下狐赡,也可以看到這樣一個結(jié)構(gòu):
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
這和C++源碼中的結(jié)構(gòu)體極為相似疟丙,也證實了NSObject對象的本質(zhì)就是一個C++結(jié)構(gòu)體享郊。
我們把NSObject_IMPL這個結(jié)構(gòu)體復(fù)制到main.m文件中:
#import <Foundation/Foundation.h>
struct NSObject_IMPL {
Class isa;//在64位中占8字節(jié)展蒂,32位中占4字節(jié)锰悼。
};
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
return 0;
}
}
然后我們按住command鍵點擊Class進入窺探一下這個Class到底是個什么東東箕般,我們看到這樣一個結(jié)構(gòu):
typedef struct objc_class *Class;
這說明Class是一個結(jié)構(gòu)體指針隘世。所以isa也就是一個指針丙者。因此NSObject_IMPL這個結(jié)構(gòu)體中就是包含了一個結(jié)構(gòu)體指針isa械媒,它所占的內(nèi)存大小就是這個isa指針?biāo)嫉膬?nèi)存大小纷捞。
在64位環(huán)境中主儡,指針占8個字節(jié)糜值,在32位環(huán)境中病往,指針占4個字節(jié)停巷。
NSObject_IMPL這個結(jié)構(gòu)體只有一個成員isa指針畔勤,所以結(jié)構(gòu)體的地址就是存放isa指針的地址硼被。比如isa這個指針的地址是0x100400100,那么就有objc=0x100400110嚷硫。
所以一個NSObject對象在64位環(huán)境中占8字節(jié)仔掸,在32位環(huán)境中占4字節(jié)起暮。我們接著往下看负懦,通過讀取內(nèi)存來驗證我們的想法纸厉。
- class_getInstanceSize()方法
class_getInstanceSize()返回的NSObject_IMPL的大小颗品。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
struct NSObject_IMPL {
Class isa;
};
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
//獲得NSObject類的NSObject_IMPL結(jié)構(gòu)體的大小
NSLog(@"class: %zd", class_getInstanceSize([NSObject class]));
return 0;
}
}
打印結(jié)果:
2018-06-25 21:09:04.070852+0800 interview1-OC對象的本質(zhì)[16368:450669] class: 8
我們查看一下class_getInstanceSize
的具體實現(xiàn),看看它獲取的到底是什么占用的內(nèi)存锄蹂,我們從runtime的源碼中可以找到class_getInstanceSize
的實現(xiàn):
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
然后我們繼續(xù)點進這個alignedInstanceSize()
里面看看:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
ivar是成員變量的意思得糜,通過注釋我們大概知道這個函數(shù)獲取的是結(jié)構(gòu)體的成員變量所占的內(nèi)存的大小,也即是NSObject_IMPL這個結(jié)構(gòu)體的大小。
下面我們回答一下第一個面試題:
- 一個NSObject對象占用多少內(nèi)存?
在32位系統(tǒng)中占4字節(jié)槽棍,在64位系統(tǒng)中占8字節(jié)炼七。
我們還可以通過xcode自帶的工具來驗證我們剛才的結(jié)論
我們在代碼中打個斷點:
然后我們在下面可以看到:
這樣我們就可以獲得objc對象的地址為:0x604000005ff0豌拙。
然后我們在xcode菜單欄中找到Debug->Debug Workflow->View Memory,在address中輸入0x604000005ff0题暖,回車就得到:
這個xcode工具的作用就是查看從輸入的這個地址開始,后面的內(nèi)存地址的情況唯绍。我們可以看到第一排中A8,7E,3B,01,00,00,00,它們是十六進制,所以一個數(shù)字表示4位况芒,那么兩個數(shù)字組合在一起就是一個字節(jié)叶撒。所以A8 7E 3B 01 00 00 00就是8個字節(jié),按照之前得出的結(jié)論压汪,這8個字節(jié)中存放的是isa指針古瓤。
如果我們不喜歡這種圖形化工具,還可以使用LLDB指令滴须。
- memory read
例如剛才窺探從0x604000005ff0開始的內(nèi)存扔水,我們也可以用LLDB指令進行:
memory read 0x604000005ff0
同樣也能得出:
memory write還可以簡寫為x魔市,即memory read 0x604000005ff0
等同于x 0x604000005ff0
待德。 - memory write
有memory read就有memory write将宪,如果我們想改變內(nèi)存中指定內(nèi)存地址的值,可以使用memory write印蔗。比如华嘹,我們使用的地址是0x604000005ff0耙厚,那么我們想改變從這個基地址開始的第9個字節(jié)內(nèi)的值颜曾,我們可以這樣寫:
memory write 0x604000005ff8 8
泛豪,然后我們x 0x604000005ff0
檢查一下:
指定內(nèi)存中的值確實修改了。 - p,po
p是print的簡寫略水,它可以用來打印非對象類型的數(shù)據(jù)慎璧,比如讀取int跨释,bool類型的值鳖谈。
po是print object的簡寫缆娃,它是用來打印對象的瑰排,比如我們使用po object
看看得到什么:
Student對象
下面我們來看一下一個更復(fù)雜的OC對象-Student對象。Student對象有兩個成員變量_no和_age字逗。
那么一個Student類的實例對象占有多少內(nèi)存呢?大家心里可能都有了自己的答案挖息。
@interface Student:NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
Student *student = [[Student alloc] init];
return 0;
}
}
同樣套腹,我們還是把main.m文件轉(zhuǎn)化為C++的源碼电禀。我們在main.cpp中通過command+f搜索Student_IMPL這個東西尖飞,我們?yōu)槭裁匆阉鬟@個東西呢政基?因為我們在學(xué)習(xí)NSObject對象時找到了NSObject_IMPL這個結(jié)構(gòu)體沮明,果然荐健,我們也找到了Student_IMPL這個結(jié)構(gòu)體:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
NSObject_IMPL其實我們已經(jīng)很熟悉了,我們還是點進去看看:
struct NSObject_IMPL {
Class isa;
};
所以Student_IMPL這個結(jié)構(gòu)體的第一個成員就是一個NSObject_IMPL結(jié)構(gòu)體扛稽,第二個第三個成員分別是Student類的成員變量在张。由于NSObject_IMPL這個結(jié)構(gòu)體就占8字節(jié),它里面的成員isa也是占8個字節(jié)啄骇,那么Student_IMPL結(jié)構(gòu)體就可以改寫成下面這樣:
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
所以我們知道一個Student的實例對象在內(nèi)存中占8+4+4=16個字節(jié)空間缸夹。并且三塊內(nèi)存空間是連續(xù)的。假設(shè)isa的地址是0x100400110蛇尚,那么_no的地址就是0x100400118取劫,_age就是0x10040011C炮捧。那么我們怎樣驗證我們的結(jié)論呢咆课?首先使用指針給成員變量賦值:
student->_no = 4;
student->_age= 5;
然后我們在程序中打個斷點查看student指針的地址為0x600000014d10傀蚌。再利用xcode的工具查看內(nèi)存:
可以很清晰的看到紅框的八個字節(jié)存放的是isa指針,綠框的四個字節(jié)存放的是_no成員變量库继,黃框的四個字節(jié)存放的是_age成員變量箩艺。并且我們可以看到綠框中四個字節(jié)存放的內(nèi)容是04 00 00 00,這和_no成員變量的值好像很吻合宪萄,又好像有一點不對艺谆,同樣,_age成員變量也是這樣拜英。這是為什么呢静汤?
這里涉及到一個概念:大端模式和小端模式。
大端模式:較高的有效字節(jié)存放在較低的存儲器地址,較低的有效字節(jié)存放在較高的存儲器地址虫给。
小端模式:較高的有效字節(jié)存放在較高的的存儲器地址藤抡,較低的有效字節(jié)存放在較低的存儲器地址缠黍。
Mac OS系統(tǒng)使用的是大端模式贸典。所以較高的有效字節(jié)存儲在較低的存儲器地址,所以04 00 00 00的正確值就是00 00 00 04即4班利。
下面我們再用另外一種方式來證明我們的結(jié)論积蜻,我們使用在NSObject對象中使用過的class_getInstanceSize()
讀取Student_IMPL所占的存儲空間:
//獲得student實例對象的成員變量所占的大小
NSLog(@"student實例對象的成員變量所占的存儲空間:%zd", class_getInstanceSize([Student class]));
輸出結(jié)果:
2018-06-26 18:33:36.642604+0800 interview1-OC對象的本質(zhì)Student[11339:336714] student實例對象所占的存儲空間:16
輸出結(jié)果再次證明了我們剛才的結(jié)論谢澈!
student實例對象的內(nèi)存結(jié)構(gòu)大概就是下圖這樣:
對擁有Person父類的Student對象的分析
@interface Person:NSObject
{
int _age;
}
@end
@implementation Person
@end
@interface Student:Person
{
@public
int _no;
}
@end
@implementation Student
@end
Student類繼承自Person類,Person類又繼承自NSObject類础芍,Person類有一個成員變量_age虏缸,Student類有一個成員變量_no宰缤。那么問題來了氧骤,Student實例對象和Person實例對象在內(nèi)存中各占多少存儲空間呢?
首先我們不把代碼轉(zhuǎn)化為C++的源碼,根據(jù)前面對NSObject對象和Student對象的分析弄砍,我們可以構(gòu)建下圖:
下面我們把main.m轉(zhuǎn)化為C++的源碼驗證一下
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;//8個字節(jié)
int _age; //4個字節(jié)
};
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _no;
};
這和我們預(yù)期的是完全一樣的型奥。
首先我們來分析一下Person實例對象占多少存儲空間:
我們知道一個NSObject_IMPL結(jié)構(gòu)體占8字節(jié)烫葬,一個int型的成員變量占4字節(jié),那么是不是一個Person實例對象就占12字節(jié)的空間呢?實際上不是的。原因有二:
- 1.一個OC對象至少占有16字節(jié)的存儲空間,低于16字節(jié)是肯定不對的计维。
- 2.有一個原則叫內(nèi)存對齊欠母,簡而言之就是一個結(jié)構(gòu)體的空間大小一定是其占有內(nèi)存空間最大的成員變量的內(nèi)存的整數(shù)倍。Person_IMPL結(jié)構(gòu)體占內(nèi)存最大的成員變量是struct NSObject_IMPL NSObject_IVARS,所以Person對象所占內(nèi)存應(yīng)該是8的倍數(shù)旨剥,結(jié)合還有一個成員變量的大小是4字節(jié)阵谚,所以Person對象所占內(nèi)存空間大小就是16字節(jié)嗡午。
我們再來分析Student對象:
Student_IMPL有兩個成員變量,其中Person_IVARS這個成員變量劝篷,我們已經(jīng)分析過了蕊蝗,占16字節(jié)幢泼,而_no這個成員變量占4字節(jié)虱饿,然后再結(jié)合內(nèi)存對齊原則爽冕,Student_IMPL結(jié)構(gòu)體就是占32字節(jié)贰谣,事實上是不是這樣呢?其實這樣分析是有問題的。
問題就出在,Person_IMPL這個結(jié)構(gòu)體占用的16個字節(jié)其實沒有全部利用疲扎,而是為了滿足內(nèi)存對齊原則等盹廷。其實在這16字節(jié)的最后4字節(jié)是空出來沒有被利用的,下圖是其內(nèi)存結(jié)構(gòu),灰色部分是空閑的佳头。
那么對于Student_IMPL的_no成員變量來說辆毡,它的存儲位置是接在灰色區(qū)域之后菜秦,把灰色區(qū)域繼續(xù)空出來還是把灰色區(qū)域利用起來呢?答案是把灰色區(qū)域利用起來舶掖。Student_IMPL的內(nèi)存結(jié)構(gòu)如下圖:
所以一個Student實例對象所占的內(nèi)存空間也是16字節(jié)球昨。
Student *student = [[Student alloc] init];
Person *person = [[Person alloc] init];
//獲得student實例對象的成員變量所占的大小
NSLog(@"student實例對象的成員變量所占的存儲空間:%zd", class_getInstanceSize([Student class]));
//獲得person實例對象的成員變量所占的大小
NSLog(@"person實例對象的成員變量所占的存儲空間:%zd", class_getInstanceSize([Person class]));
打印結(jié)果:
2018-06-26 19:33:52.467400+0800 interview1-OC對象的本質(zhì)Student[12656:386270] student實例對象的成員變量所占的存儲空間:16
2018-06-26 19:33:52.468997+0800 interview1-OC對象的本質(zhì)Student[12656:386270] person實例對象的成員變量所占的存儲空間:16
打印結(jié)果也就驗證了我們的推測。
屬性和方法
我們給Person類增加一個height屬性眨攘。
@interface Person:NSobject
{
@public
int _no;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
@end
那么Person_IMPL結(jié)構(gòu)體會變成什么樣子呢主慰?轉(zhuǎn)化后找到Person_IMPL:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
我們可以看到增加了_height成員變量,這和我們所學(xué)的OC知識:聲明一個屬性的同時也就聲明了一個成員變量是一致的鲫售。
我們創(chuàng)建出來的實例對象中只有成員變量共螺,為什么沒有存放方法呢?
每個實例對象中都有一份成員變量情竹,因為每個實例對象都可以有自己的成員變量值藐不,每個實例對象的成員變量值都可以不一樣,所以需要在每個實例對象中存放所有的成員變量秦效。但是方法就不一樣了雏蛮,每個對象執(zhí)行的方法都是一樣的,只需要保存一份就夠了阱州,沒有必要在每個實例對象中都保留一份方法挑秉。