OC對象的本質<一>

面試問題:

  • 一個NSObject對象占用多少內存?
  • 對象的isa指針指向哪里?
  • OC的類信息存放在哪里?
int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        return 0;
    }
}

第一個問題實質上就可以轉化為objc這個指針指向的內存區域有多大。為了搞清這個問題,我們就要搞清楚NSObject在內存中是怎么布局的,它的底層原理。

Objective-c的本質

我們平時編寫的objective-c代碼,底層實現其實都是C/C++代碼

Objective-C > C/C++ > 匯編語言 > 機器語言

所以Objective-C的面向對象都是基于C/C++的數據結構實現的。
Objective-C的對象,類主要是基于C/C++的結構體來實現的。

  • 將Objective-c代碼轉化為C/C++的代碼:
    1>在命令行cd到放Objective-c代碼的文件夾
    2>比如我們要把文件夾中的main.m文件轉化,我們可以再命令行輸入:clang -rewrite-objc main.m,然后在這個文件夾下我們就得到了轉化成功的文件main.cpp。
    我們將上面的代碼轉化為C++的源碼,得到main.cpp。
    在7000多行我們找到這樣一個結構體:
//NSObject implemention
struct NSObject_IMPL {
    Class isa;
};

這個結構體就是NSObject對象在內存中的本質。
另外,我們按住command點擊進NSObject里面看一下,也可以看到這樣一個結構:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

這和C++源碼中的結構體極為相似,也證實了NSObject對象的本質就是一個C++結構體。
我們把NSObject_IMPL這個結構體復制到main.m文件中:

#import <Foundation/Foundation.h>

struct NSObject_IMPL {
    Class isa;//在64位中占8字節,32位中占4字節。
};

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        return 0;
    }
}

然后我們按住command鍵點擊Class進入窺探一下這個Class到底是個什么東東,我們看到這樣一個結構:

typedef struct objc_class *Class;

這說明Class是一個結構體指針。所以isa也就是一個指針。因此NSObject_IMPL這個結構體中就是包含了一個結構體指針isa,它所占的內存大小就是這個isa指針所占的內存大小。
在64位環境中,指針占8個字節,在32位環境中,指針占4個字節。
NSObject_IMPL這個結構體只有一個成員isa指針,所以結構體的地址就是存放isa指針的地址。比如isa這個指針的地址是0x100400100,那么就有objc=0x100400110。

所以一個NSObject對象在64位環境中占8字節,在32位環境中占4字節。我們接著往下看,通過讀取內存來驗證我們的想法。

  • 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結構體的大小
        NSLog(@"class: %zd", class_getInstanceSize([NSObject class]));
        return 0;
    }
}

打印結果:

2018-06-25 21:09:04.070852+0800 interview1-OC對象的本質[16368:450669] class: 8

我們查看一下class_getInstanceSize的具體實現,看看它獲取的到底是什么占用的內存,我們從runtime的源碼中可以找到class_getInstanceSize的實現:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

然后我們繼續點進這個alignedInstanceSize()里面看看:

// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

ivar是成員變量的意思,通過注釋我們大概知道這個函數獲取的是結構體的成員變量所占的內存的大小,也即是NSObject_IMPL這個結構體的大小。
下面我們回答一下第一個面試題:

  • 一個NSObject對象占用多少內存?
    在32位系統中占4字節,在64位系統中占8字節。
我們還可以通過xcode自帶的工具來驗證我們剛才的結論

我們在代碼中打個斷點:


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工具的作用就是查看從輸入的這個地址開始,后面的內存地址的情況。我們可以看到第一排中A8,7E,3B,01,00,00,00,它們是十六進制,所以一個數字表示4位,那么兩個數字組合在一起就是一個字節。所以A8 7E 3B 01 00 00 00就是8個字節,按照之前得出的結論,這8個字節中存放的是isa指針。

如果我們不喜歡這種圖形化工具,還可以使用LLDB指令。
  • memory read
    例如剛才窺探從0x604000005ff0開始的內存,我們也可以用LLDB指令進行:
    memory read 0x604000005ff0同樣也能得出:
    5B5ABF87-B7FD-4C5B-A504-0C36C5D4F3F4.png

    memory write還可以簡寫為x,即memory read 0x604000005ff0等同于x 0x604000005ff0
  • memory write
    有memory read就有memory write,如果我們想改變內存中指定內存地址的值,可以使用memory write。比如,我們使用的地址是0x604000005ff0,那么我們想改變從這個基地址開始的第9個字節內的值,我們可以這樣寫:
    memory write 0x604000005ff8 8,然后我們x 0x604000005ff0檢查一下:
    36C8CCE5-7688-4F8C-BADC-4433AF5B98A7.png

    指定內存中的值確實修改了。
  • p,po
    p是print的簡寫,它可以用來打印非對象類型的數據,比如讀取int,bool類型的值。
    po是print object的簡寫,它是用來打印對象的,比如我們使用po object看看得到什么:
    505E2E1B-2590-4E58-B27A-3ED3BC8C789D.png
Student對象

下面我們來看一下一個更復雜的OC對象-Student對象。Student對象有兩個成員變量_no和_age。
那么一個Student類的實例對象占有多少內存呢?大家心里可能都有了自己的答案。

@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文件轉化為C++的源碼。我們在main.cpp中通過command+f搜索Student_IMPL這個東西,我們為什么要搜索這個東西呢?因為我們在學習NSObject對象時找到了NSObject_IMPL這個結構體,果然,我們也找到了Student_IMPL這個結構體:

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

NSObject_IMPL其實我們已經很熟悉了,我們還是點進去看看:

struct NSObject_IMPL {
    Class isa;
};

所以Student_IMPL這個結構體的第一個成員就是一個NSObject_IMPL結構體,第二個第三個成員分別是Student類的成員變量。由于NSObject_IMPL這個結構體就占8字節,它里面的成員isa也是占8個字節,那么Student_IMPL結構體就可以改寫成下面這樣:

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

所以我們知道一個Student的實例對象在內存中占8+4+4=16個字節空間。并且三塊內存空間是連續的。假設isa的地址是0x100400110,那么_no的地址就是0x100400118,_age就是0x10040011C。那么我們怎樣驗證我們的結論呢?首先使用指針給成員變量賦值:

student->_no = 4;
student->_age= 5;

然后我們在程序中打個斷點查看student指針的地址為0x600000014d10。再利用xcode的工具查看內存:

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

可以很清晰的看到紅框的八個字節存放的是isa指針,綠框的四個字節存放的是_no成員變量,黃框的四個字節存放的是_age成員變量。并且我們可以看到綠框中四個字節存放的內容是04 00 00 00,這和_no成員變量的值好像很吻合,又好像有一點不對,同樣,_age成員變量也是這樣。這是為什么呢?
這里涉及到一個概念:大端模式和小端模式。

大端模式:較高的有效字節存放在較低的存儲器地址,較低的有效字節存放在較高的存儲器地址。
小端模式:較高的有效字節存放在較高的的存儲器地址,較低的有效字節存放在較低的存儲器地址。

Mac OS系統使用的是大端模式。所以較高的有效字節存儲在較低的存儲器地址,所以04 00 00 00的正確值就是00 00 00 04即4。
下面我們再用另外一種方式來證明我們的結論,我們使用在NSObject對象中使用過的class_getInstanceSize()讀取Student_IMPL所占的存儲空間:

//獲得student實例對象的成員變量所占的大小
 NSLog(@"student實例對象的成員變量所占的存儲空間:%zd", class_getInstanceSize([Student class]));

輸出結果:

2018-06-26 18:33:36.642604+0800 interview1-OC對象的本質Student[11339:336714] student實例對象所占的存儲空間:16

輸出結果再次證明了我們剛才的結論!
student實例對象的內存結構大概就是下圖這樣:


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實例對象在內存中各占多少存儲空間呢?
首先我們不把代碼轉化為C++的源碼,根據前面對NSObject對象和Student對象的分析,我們可以構建下圖:


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

下面我們把main.m轉化為C++的源碼驗證一下

struct NSObject_IMPL {
    Class isa;
};
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;//8個字節
    int _age;     //4個字節
};
struct Student_IMPL {
    struct Person_IMPL Person_IVARS;
    int _no;
};

這和我們預期的是完全一樣的。
首先我們來分析一下Person實例對象占多少存儲空間:
我們知道一個NSObject_IMPL結構體占8字節,一個int型的成員變量占4字節,那么是不是一個Person實例對象就占12字節的空間呢?實際上不是的。原因有二:

  • 1.一個OC對象至少占有16字節的存儲空間,低于16字節是肯定不對的。
  • 2.有一個原則叫內存對齊簡而言之就是一個結構體的空間大小一定是其占有內存空間最大的成員變量的內存的整數倍。Person_IMPL結構體占內存最大的成員變量是struct NSObject_IMPL NSObject_IVARS,所以Person對象所占內存應該是8的倍數,結合還有一個成員變量的大小是4字節,所以Person對象所占內存空間大小就是16字節。
    我們再來分析Student對象:
    Student_IMPL有兩個成員變量,其中Person_IVARS這個成員變量,我們已經分析過了,占16字節,而_no這個成員變量占4字節,然后再結合內存對齊原則,Student_IMPL結構體就是占32字節,事實上是不是這樣呢?其實這樣分析是有問題的。
    問題就出在,Person_IMPL這個結構體占用的16個字節其實沒有全部利用,而是為了滿足內存對齊原則等。其實在這16字節的最后4字節是空出來沒有被利用的,下圖是其內存結構,灰色部分是空閑的。
    0D160963-EA4A-474E-A991-04CD9F165104.png

    那么對于Student_IMPL的_no成員變量來說,它的存儲位置是接在灰色區域之后,把灰色區域繼續空出來還是把灰色區域利用起來呢?答案是把灰色區域利用起來。Student_IMPL的內存結構如下圖:
    EAC31B8D-F806-4F09-A242-BB9A0B5FD008.png

    所以一個Student實例對象所占的內存空間也是16字節。
        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]));

打印結果:

2018-06-26 19:33:52.467400+0800 interview1-OC對象的本質Student[12656:386270] student實例對象的成員變量所占的存儲空間:16
2018-06-26 19:33:52.468997+0800 interview1-OC對象的本質Student[12656:386270] person實例對象的成員變量所占的存儲空間:16

打印結果也就驗證了我們的推測。

屬性和方法

我們給Person類增加一個height屬性。

@interface Person:NSobject
{
    
    @public
    int _no;
}
@property (nonatomic, assign) int height;

@end

@implementation Person
@end

那么Person_IMPL結構體會變成什么樣子呢?轉化后找到Person_IMPL:

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
};

我們可以看到增加了_height成員變量,這和我們所學的OC知識:聲明一個屬性的同時也就聲明了一個成員變量是一致的。
我們創建出來的實例對象中只有成員變量,為什么沒有存放方法呢?
每個實例對象中都有一份成員變量,因為每個實例對象都可以有自己的成員變量值,每個實例對象的成員變量值都可以不一樣,所以需要在每個實例對象中存放所有的成員變量。但是方法就不一樣了,每個對象執行的方法都是一樣的,只需要保存一份就夠了,沒有必要在每個實例對象中都保留一份方法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容