NSObject 對象的內(nèi)存布局

前言

Objective-C語言是一門高級語言指攒,底層是由C/C++語言實現(xiàn)蒿往。要想從本質(zhì)上了解Objective-C對象的底層數(shù)據(jù)結(jié)構(gòu)和內(nèi)存布局咽块,就需要一步步揭開那最神秘的面紗。

Objective-C對象經(jīng)過編譯鏈接運行后锭碳,所經(jīng)歷的過程如下所示:


image.png

在后面的講解中,主要將Objective-C對象一步步轉(zhuǎn)為最底層的實現(xiàn)勿璃。

將Objective-C語言轉(zhuǎn)換為C/C++語言

在終端執(zhí)行下面的命令擒抛,可以將Objective-C對象轉(zhuǎn)換成C/C++語言:

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

舉例說明:

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

        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

執(zhí)行上述命令后,得到的結(jié)果如下:

 struct NSObject_IMPL {
     Class isa;
 };
 
 int main(int argc, const char * argv[]) {
     /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
 
         NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
     }
    return 0;
}

通過上述編譯后的代碼可以看出补疑,NSObject對象的底層數(shù)據(jù)結(jié)構(gòu)是結(jié)構(gòu)體歧沪。

struct NSObject_IMPL {
    Class isa;
};

如何獲取NSObject對象的內(nèi)存大小莲组?

獲取NSObject對象的內(nèi)存大小诊胞,需要用到以下幾個函數(shù):

  • class_getInstanceSize
  • malloc_size
  • sizeOf
    NSObject *obj = [[NSObject alloc] init];
    
    NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
    NSLog(@"malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
    NSLog(@"sizeOf = %zd", sizeof(obj));

控制臺打印如下:

class_getInstanceSize = 8
malloc_size = 16
sizeOf = 8

獲取結(jié)果居然不一樣,那是為什么呢锹杈?那就繼續(xù)探究一下源碼實現(xiàn)吧撵孤!

1、class_getInstanceSize

這個是一個runtime提供的API竭望,用于獲取類實例對象所占用的內(nèi)存大小邪码,返回所占用的字節(jié)數(shù)。

在蘋果開源網(wǎng)站咬清,找到對應的objc4-779.1.zip壓縮包闭专∨耍看一下源碼實現(xiàn),在objc-class.mm文件到找到了該方法的實現(xiàn)影钉,如下所示:

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

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

Class's ivar size rounded up to a pointer-size boundary

翻譯一下画髓,返回實例對象中成員變量內(nèi)存大小。說白了平委,class_getInstanceSize就是獲取實例對象中成員變量內(nèi)存大小雀扶。

仔細想一下,實例對象在創(chuàng)建的時候肆汹,系統(tǒng)應該就會分配對應的內(nèi)存空間愚墓,那在對象初始化的過程中,是否有對應的內(nèi)存分配呢昂勉?

2浪册、alloc

我們都知道初始化一個OC對象是有兩個步驟:

  1. 給對象分配一個內(nèi)存空間
  2. 初始化該對象

當我們 alloc 的時候系統(tǒng)會分配內(nèi)存空間(地址)給OC對象,當 init 的時候?qū)崿F(xiàn)了對象的初始化工作岗照。就完成了一個對象的創(chuàng)建過程村象。

當執(zhí)行alloc的時候,系統(tǒng)會自動調(diào)用分配內(nèi)存地址的方法:

對象的創(chuàng)建離不開alloc方法攒至,對象創(chuàng)建的過程中可能存在分配內(nèi)存空間的方法厚者,一起看下源碼。

NSObject.mm類中找到alloc以及allocFromZone方法的實現(xiàn):

+ (id)alloc {
    return _objc_rootAlloc(self);
}

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

找到時機調(diào)用的核心方法是:_objc_rootAllocWithZone

id  _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

繼續(xù)查找:_class_createInstanceFromZone

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);  // 
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

在調(diào)用calloc或者malloc_zone_calloc函數(shù)是需要傳入size參數(shù)迫吐,可以發(fā)現(xiàn)size變量來源于下面的代碼:

    size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

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

CF requires all objects be at least 16 bytes.

CoreFoundation 框架要求所有對象至少分配16個字節(jié)库菲。

當實例對象不足16個字節(jié),系統(tǒng)分配給16個字節(jié)志膀,屬于系統(tǒng)的硬性規(guī)定熙宇。

仔細看,會發(fā)現(xiàn)alignedInstanceSize函數(shù)不就是class_getInstanceSize函數(shù)的內(nèi)部實現(xiàn)溉浙。

3烫止、malloc_size

這個函數(shù)主要獲取 系統(tǒng)實際分配的內(nèi)存大小,具體的底層實現(xiàn)也可以在源碼libmalloc找到

4戳稽、sizeOf

值得注意的一點是馆蠕,sizeof是操作符,不是函數(shù)惊奇,它的作用對象是數(shù)據(jù)類型互躬,主要作用于編譯時。

因此赊时,它作用于變量時吨铸,也是對 其類型 進行操作。得到的結(jié)果是該數(shù)據(jù) 類型 占用空間大小祖秒,即size_t類型诞吱。

5舟奠、應用

通過上面的學習,我們可以很好回答下面的這個經(jīng)典的問題了:

一個NSObject對象占用多少內(nèi)存房维?

在64位架構(gòu)下沼瘫, 系統(tǒng)分配了16個字節(jié)給NSObject對象(通過malloc_size函數(shù)獲得);
但NSObject對象內(nèi)部只使用了8個字節(jié)的空間(可以通過class_getInstanceSize函數(shù)獲得)咙俩。

內(nèi)存對齊

1耿戚、內(nèi)存對齊是什么?

內(nèi)存對齊 是一種在計算機內(nèi)存中 排列數(shù)據(jù)(表現(xiàn)為變量的地址)阿趁、訪問數(shù)據(jù)(表現(xiàn)為CPU讀取數(shù)據(jù))的一種方式膜蛔。

它包含了兩種相互獨立又相互關聯(lián)的部分:基本數(shù)據(jù)對齊結(jié)構(gòu)體數(shù)據(jù)對齊

在iOS開發(fā)過程中脖阵,編譯器會自動的進行字節(jié)對齊的處理皂股,并且在64位架構(gòu)下,是以8字節(jié)進行內(nèi)存對齊的命黔。

2呜呐、內(nèi)存對齊的原則

內(nèi)存對齊應該是編譯器的管轄范圍,編譯器為程序中的每個數(shù)據(jù)單元安排在適當?shù)奈恢蒙虾纺迹奖阌嬎銠C快速高效的進行讀取數(shù)據(jù)蘑辑。

每個平臺的編譯器都有自己的對齊系數(shù)和相應的對齊規(guī)則。在iOS中的64位架構(gòu)下坠宴,對齊系數(shù)就是8個字節(jié)洋魂。

注意: 內(nèi)存對齊有實際占用的內(nèi)存對齊,也有系統(tǒng)分配內(nèi)存對齊啄踊,iOS中的64位架構(gòu)下忧设,系統(tǒng)分配對齊系數(shù)是16個字節(jié)荆萤,比如一個 NSObject 對象實際占用8個字節(jié)彩扔,但是系統(tǒng)分配16個字節(jié)

例如:代碼申請4個字節(jié)的空間猪瞬,但是因為內(nèi)存對齊,系統(tǒng)實際分配了16個字節(jié)

    void *p = malloc(4);
    NSLog(@"%zd", malloc_size(p));

    [7436:1197123] 16

2.1 數(shù)據(jù)成員對齊

結(jié)構(gòu)體或者共用體中的成員變量中顿锰,首個成員變量放在偏移量為0的位置上,后面的成員變量的對齊偏移量是取指定對齊系數(shù)和本身該成員變量所占用大小中的較小值启搂,即 min(對齊系數(shù),成員變量的內(nèi)存大小 )

2.2 數(shù)據(jù)整體對齊

在結(jié)構(gòu)體或者共用體中的成員變量完成自身的對齊之后硼控,整個結(jié)構(gòu)體或者共用體也需要進行字節(jié)對齊處理,一般為 min(對齊系數(shù),最大成員變量的內(nèi)存大小 )的整數(shù)倍胳赌。

結(jié)合上述原則1牢撼、2,可以推斷出下面的常用原則疑苫,以結(jié)構(gòu)體為例:

  • 結(jié)構(gòu)體變量的首地址是其最長基本類型成員的整數(shù)倍熏版;
  • 結(jié)構(gòu)體每個成員相對于結(jié)構(gòu)體首地址的偏移量(offset)都是成員大小的整數(shù)倍纷责,如不滿足,對前一個成員填充字節(jié)以滿足撼短;
  • 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最大基本類型成員變量大小的整數(shù)倍再膳;
  • 結(jié)構(gòu)體中的成員變量都是分配在連續(xù)的內(nèi)存空間中。

在熟悉上述對齊原則基礎上曲横,默認在64位架構(gòu)下喂柒,舉個例子:

struct object {
    int a; // 4
    NSString *b; // 8
    int c; // 4
    char d; // 1
};

結(jié)構(gòu)體中最大的成員變量占用8個字節(jié),根據(jù)上面的對齊原則禾嫉,最終獲得的對齊系數(shù)是min(最大成員變量大小8個字節(jié), 對齊系數(shù)8個字節(jié)) = 8灾杰。

不考慮內(nèi)存對齊的情況下,實際占用4 + 8 + 4 + 1 = 17個字節(jié)熙参,考慮字節(jié)對齊的情況下艳吠,分配24個字節(jié)。

例2:

struct object {
    int a; // 4
    char b; // 1
    int c; // 4
};

根據(jù)上面結(jié)構(gòu)體尊惰,可以得出需要對齊的字節(jié)數(shù)為min(對齊系數(shù), 最大成員變量的內(nèi)存大小) = 4個字節(jié)讲竿。對齊后的內(nèi)存分配表如下所示:


image.png

3、內(nèi)存對齊的原因

為了減少CPU訪問內(nèi)存的次數(shù),提高計算機性能,一些計算機硬件平臺要求存儲在內(nèi)存中的變量按自然邊界對齊弄屡。

3.1 性能上的提升
3.2 跨平臺

4题禀、內(nèi)存對齊的注意事項

4.1 內(nèi)存分配

在結(jié)構(gòu)體中,聲明成員變量的順序不一致膀捷,也會導致最終分配內(nèi)存大小的不同迈嘹。

struct object {
    int a; // 4
    NSString *b; // 8
    int c; // 4
};

對齊情況下,系統(tǒng)分配24個字節(jié)全庸,具體分配如下:


image.png

調(diào)換一下成員變量的聲明順序:

struct object {
    int a; // 4
    int c; // 4
    NSString *b; // 8
};

這種情況下秀仲,系統(tǒng)分配16個字節(jié),具體分配如下:


image.png

通過上面對比可以看出壶笼,在日常開發(fā)中神僵,設計結(jié)構(gòu)的時候,合理調(diào)換成員變量的順序覆劈,可以很好地節(jié)省內(nèi)存空間保礼。

OC對象的內(nèi)存布局

情景一:帶有一個成員變量的對象占用內(nèi)存的大小

@interface Animal : NSObject
{
    @public
    int _age;
}
@end

    Animal *animal = [[Animal alloc] init];
    NSLog(@"Animal -- class_getInstanceSize = %zd", class_getInstanceSize([animal class]));
    NSLog(@"Animal -- malloc_size = %zd", malloc_size((__bridge const void *)(animal)));
    NSLog(@"Animal -- sizeOf = %zd", sizeof(animal));

2020-04-25 13:34:44.353148+0800 ClangDemo[7571:1234217] Animal -- class_getInstanceSize = 16
2020-04-25 13:34:44.353241+0800 ClangDemo[7571:1234217] Animal -- malloc_size = 16
2020-04-25 13:34:44.353264+0800 ClangDemo[7571:1234217] Animal -- sizeOf = 8

情景二:不同成員變量的對象占用內(nèi)存的大小
在情景一的基礎上,在Animal對象再添加一個成員變量_weight责语,如下所示:

@interface Animal : NSObject
{
    @public
    int _age;
    int _weight;
}
@end

2020-04-25 13:42:41.791946+0800 ClangDemo[7575:1236418] Animal -- class_getInstanceSize = 16
2020-04-25 13:42:41.792034+0800 ClangDemo[7575:1236418] Animal -- malloc_size = 16
2020-04-25 13:42:41.792058+0800 ClangDemo[7575:1236418] Animal -- sizeOf = 8

情景三:繼續(xù)添加不同類型的成員變量
添加整型成員變量

@interface Animal : NSObject
{
    @public
    int _age;
    int _weight;
    int _height;
}
@end

2020-04-25 13:44:27.894359+0800 ClangDemo[7577:1237260] Animal -- class_getInstanceSize = 24
2020-04-25 13:44:27.894433+0800 ClangDemo[7577:1237260] Animal -- malloc_size = 32
2020-04-25 13:44:27.894453+0800 ClangDemo[7577:1237260] Animal -- sizeOf = 8

添加字符串型成員變量

NSString *_name;

2020-04-25 13:46:12.214979+0800 ClangDemo[7581:1238086] Animal -- class_getInstanceSize = 32
2020-04-25 13:46:12.215062+0800 ClangDemo[7581:1238086] Animal -- malloc_size = 32
2020-04-25 13:46:12.215085+0800 ClangDemo[7581:1238086] Animal -- sizeOf = 8

情景四:調(diào)換成員變量聲明順序
情況一:整型變量中摻雜字符串變量

    int _age;
     NSString *_name;
     int _weight;
     NSString *_nick;
     int _height;

2020-04-25 13:47:56.280581+0800 ClangDemo[7584:1238907] Animal -- class_getInstanceSize = 48
2020-04-25 13:47:56.280654+0800 ClangDemo[7584:1238907] Animal -- malloc_size = 48
2020-04-25 13:47:56.280675+0800 ClangDemo[7584:1238907] Animal -- sizeOf = 8

情況二:調(diào)換一下聲明成員變量的順序

    int _age;
     int _weight;
     int _height;
     NSString *_name;
     NSString *_nick;

2020-04-25 13:49:21.888761+0800 ClangDemo[7588:1239645] Animal -- class_getInstanceSize = 40
2020-04-25 13:49:21.888838+0800 ClangDemo[7588:1239645] Animal -- malloc_size = 48
2020-04-25 13:49:21.888860+0800 ClangDemo[7588:1239645] Animal -- sizeOf = 8

情景五:繼承體系下的內(nèi)存分配

@interface Animal : NSObject
{
    @public
    int _age;
     int _weight;
}
@end

@interface Dog : Animal
{
    @public
    int _height;
}

2020-04-25 13:53:47.886732+0800 ClangDemo[7592:1240968] Dog -- class_getInstanceSize = 24
2020-04-25 13:53:47.886810+0800 ClangDemo[7592:1240968] Dog -- malloc_size = 32
2020-04-25 13:53:47.886831+0800 ClangDemo[7592:1240968] Dog -- sizeOf = 8

參考文章

關于NSObject對象的內(nèi)存布局炮障,看我就夠了!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坤候,一起剝皮案震驚了整個濱河市胁赢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌白筹,老刑警劉巖智末,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谅摄,死亡現(xiàn)場離奇詭異,居然都是意外死亡吹害,警方通過查閱死者的電腦和手機螟凭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來它呀,“玉大人螺男,你說我怎么就攤上這事∽荽” “怎么了下隧?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谓媒。 經(jīng)常有香客問我淆院,道長,這世上最難降的妖魔是什么句惯? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任土辩,我火速辦了婚禮,結(jié)果婚禮上抢野,老公的妹妹穿的比我還像新娘拷淘。我一直安慰自己,他們只是感情好指孤,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布启涯。 她就那樣靜靜地躺著,像睡著了一般恃轩。 火紅的嫁衣襯著肌膚如雪结洼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天叉跛,我揣著相機與錄音松忍,去河邊找鬼。 笑死筷厘,一個胖子當著我的面吹牛挽铁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播敞掘,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼楣铁!你這毒婦竟也來了玖雁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤盖腕,失蹤者是張志新(化名)和其女友劉穎赫冬,沒想到半個月后浓镜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡劲厌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年膛薛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片补鼻。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡哄啄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出风范,到底是詐尸還是另有隱情咨跌,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布硼婿,位于F島的核電站锌半,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寇漫。R本人自食惡果不足惜刊殉,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望州胳。 院中可真熱鬧记焊,春花似錦、人聲如沸陋葡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腐缤。三九已至捌归,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間岭粤,已是汗流浹背惜索。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留剃浇,地道東北人巾兆。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像虎囚,于是被迫代替她去往敵國和親角塑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

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

  • 最全的iOS面試題及答案 iOS面試小貼士 ———————————————回答好下面的足夠了-----------...
    zweic閱讀 2,703評論 0 73
  • 面試題 一個 NSObject 對象占用多少內(nèi)存淘讥? 系統(tǒng)分配了16個字節(jié)給NSObject對象(通過malloc_...
    叫我小黑閱讀 560評論 0 0
  • Objective-C編程語言是C語言的超集圃伶,在C語言的基礎上加入了面向?qū)ο蟮膬?nèi)容。OC可以和C/C++混合使用,...
    Henry_Dev閱讀 5,116評論 13 31
  • 山中喜鵲筑巢 山下我拉著 你的柳條走 池塘里的魚團錦簇 在一只貓午后的夢中
    老晁閱讀 120評論 0 1
  • 道盡人間悲歡離合 演繹經(jīng)典和浪漫 文/羅漢 七月九日晚窒朋,雨后的銅城搀罢,涼風習習,天降祥瑞侥猩。百年...
    紅塵羅漢閱讀 1,357評論 8 21