OC對象的本質(zhì)<一>

面試問題:

  • 一個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é)論

我們在代碼中打個斷點:


4E40A483-E979-46FC-AE8D-24DA063AB8CA.png

然后我們在下面可以看到:


9CC80E85-7DBB-4A50-995A-625863DA1A4A.png

這樣我們就可以獲得objc對象的地址為:0x604000005ff0豌拙。
然后我們在xcode菜單欄中找到Debug->Debug Workflow->View Memory,在address中輸入0x604000005ff0题暖,回車就得到:
2E376D19-FEF4-44F2-8187-282FD9C856E2.png

這個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同樣也能得出:
    5B5ABF87-B7FD-4C5B-A504-0C36C5D4F3F4.png

    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檢查一下:
    36C8CCE5-7688-4F8C-BADC-4433AF5B98A7.png

    指定內(nèi)存中的值確實修改了。
  • p,po
    p是print的簡寫略水,它可以用來打印非對象類型的數(shù)據(jù)慎璧,比如讀取int跨释,bool類型的值鳖谈。
    po是print object的簡寫缆娃,它是用來打印對象的瑰排,比如我們使用po object看看得到什么:
    505E2E1B-2590-4E58-B27A-3ED3BC8C789D.png
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)存:

87449745-BD1F-490D-B74F-44F4108FA385.png

可以很清晰的看到紅框的八個字節(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)大概就是下圖這樣:


0A4E26FC-B041-45AD-9922-C49A8996DA13.png
對擁有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)建下圖:


15030C3C-6CB5-44D7-B6DA-A2B8ED40EE8A.png

下面我們把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),灰色部分是空閑的佳头。
    0D160963-EA4A-474E-A991-04CD9F165104.png

    那么對于Student_IMPL的_no成員變量來說辆毡,它的存儲位置是接在灰色區(qū)域之后菜秦,把灰色區(qū)域繼續(xù)空出來還是把灰色區(qū)域利用起來呢?答案是把灰色區(qū)域利用起來舶掖。Student_IMPL的內(nèi)存結(jié)構(gòu)如下圖:
    EAC31B8D-F806-4F09-A242-BB9A0B5FD008.png

    所以一個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í)行的方法都是一樣的,只需要保存一份就夠了阱州,沒有必要在每個實例對象中都保留一份方法挑秉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市苔货,隨后出現(xiàn)的幾起案子犀概,更是在濱河造成了極大的恐慌,老刑警劉巖夜惭,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姻灶,死亡現(xiàn)場離奇詭異,居然都是意外死亡滥嘴,警方通過查閱死者的電腦和手機木蹬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來若皱,“玉大人镊叁,你說我怎么就攤上這事∽叽ィ” “怎么了晦譬?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長互广。 經(jīng)常有香客問我敛腌,道長,這世上最難降的妖魔是什么惫皱? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任像樊,我火速辦了婚禮,結(jié)果婚禮上旅敷,老公的妹妹穿的比我還像新娘生棍。我一直安慰自己,他們只是感情好媳谁,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布涂滴。 她就那樣靜靜地躺著,像睡著了一般晴音。 火紅的嫁衣襯著肌膚如雪柔纵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天锤躁,我揣著相機與錄音搁料,去河邊找鬼。 笑死系羞,一個胖子當(dāng)著我的面吹牛加缘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播觉啊,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼拣宏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了杠人?” 一聲冷哼從身側(cè)響起勋乾,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嗡善,沒想到半個月后辑莫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡罩引,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年各吨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袁铐。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡揭蜒,死狀恐怖横浑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屉更,我是刑警寧澤徙融,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站瑰谜,受9級特大地震影響欺冀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萨脑,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一隐轩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渤早,春花似錦职车、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仅淑,卻和暖如春称勋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涯竟。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工赡鲜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人庐船。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓银酬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筐钟。 傳聞我的和親對象是個殘疾皇子揩瞪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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