OC對(duì)象底層探索 — 由字節(jié)對(duì)齊到對(duì)象內(nèi)存的分配

用于記錄iOS底層學(xué)習(xí),以備后續(xù)回顧

OC對(duì)象底層探索

前言

我們繼續(xù)來(lái)探索對(duì)象是如何申請(qǐng)、開(kāi)辟、優(yōu)化內(nèi)存大小的。
要想了解對(duì)象的內(nèi)存優(yōu)化首先要知道內(nèi)存對(duì)齊原則(理論加實(shí)踐一點(diǎn)點(diǎn)的搞懂)。

一、內(nèi)存對(duì)齊

1.1 內(nèi)存對(duì)齊的三個(gè)規(guī)則

  • a. 數(shù)據(jù)成員對(duì)齊規(guī)則:結(jié)構(gòu)體(struct)(或聯(lián)合體(union))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如數(shù)組、結(jié)構(gòu)體等)的整數(shù)倍開(kāi)始(比如int為4字節(jié),則要從4的整數(shù)倍地址開(kāi)始存儲(chǔ))。
  • b. 結(jié)構(gòu)體作為成員:如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開(kāi)始存儲(chǔ)(struct a里存有struct b,b里有char,int,double等元素,那b應(yīng)該從8的整數(shù)倍開(kāi)始存儲(chǔ))
  • c. 最后判斷:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足要補(bǔ)齊

1.2 內(nèi)存對(duì)齊代碼示例探究

經(jīng)過(guò)實(shí)際探究,下列示例已涵蓋內(nèi)存對(duì)齊的全部原則并加以備注

struct DZStruct1 {
    char a;     // 1    [0]
    double b;   // 8    [8, 15]
    int c;      // 4    [16, 19]
    short d;    // 2    [20, 22]
} MyStruct1; // sizeof(MyStruct1) = 24字節(jié)

struct DZStruct2 {
    double b;   // 8    [0, 7]
    char a;     // 1    [8]
    int c;      // 4    [12, 15] 按順序從9開(kāi)始,但是起始位置需要從該成員大小的整數(shù)倍開(kāi)始,所以起始位置9需要往大了走,到12為4的整數(shù)倍
    short d;    // 2    [16, 17]
} MyStruct2; // sizeof(MyStruct2) = 24字節(jié),結(jié)構(gòu)體的總大小,必須是其內(nèi)部最大成員的整數(shù)倍,不足要補(bǔ)齊所以要從17補(bǔ)齊到24為8的整數(shù)倍

struct DZStruct3 {
    char a;     // 1    [0]
    struct DZStruct1 struct1;   // 24   [8, 31] 按順序從1開(kāi)始,但是起始位置需要從該結(jié)構(gòu)體(DZStruct1)內(nèi)部最大元素大小的整數(shù)倍開(kāi)始存儲(chǔ),所以從1往大了走,到8為8的整數(shù)倍
    double b;   // 8    [32, 39]
    short d;    // 2    [40, 41]
} MyStruct3; // sizeof(MyStruct3) = 48字節(jié)

1.3 字節(jié)基礎(chǔ)補(bǔ)充

a. 先簡(jiǎn)單補(bǔ)充一點(diǎn)字節(jié)相關(guān)基礎(chǔ)知識(shí):
sizeof()是運(yùn)算符,編譯的時(shí)候就是一個(gè)確定的數(shù)據(jù)會(huì)替換為常數(shù),返回的是一個(gè)類(lèi)型所占內(nèi)存的字節(jié)大小。

b. 了解獲取內(nèi)存的三個(gè)方法

  • sizeof是運(yùn)算符,編譯的時(shí)候就替換為常數(shù),返回的是一個(gè)類(lèi)型所占內(nèi)存的大小
  • class_getInstanceSize傳入一個(gè)類(lèi)對(duì)象,返回一個(gè)對(duì)象的實(shí)例至少需要多少內(nèi)存,它等價(jià)于sizeof,需要導(dǎo)入#import <objc/runtime.h>
  • malloc_size返回系統(tǒng)實(shí)際分配的內(nèi)存大小,需要導(dǎo)入#import <malloc/malloc.h>

二、探索對(duì)象申請(qǐng)內(nèi)存和系統(tǒng)分配內(nèi)存

2.1 首先對(duì)無(wú)成員變量對(duì)象進(jìn)行探索

DZTeacher.h文件

#import <Foundation/Foundation.h>
@interface DZTeacher : NSObject

main.m文件

#import <Foundation/Foundation.h>
#import "DZTeacher.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZTeacher  *p = [DZTeacher alloc];
        NSLog(@"申請(qǐng)內(nèi)存大小為:%lu——-系統(tǒng)開(kāi)辟內(nèi)存大小為:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
    }
    return 0;
}
分析結(jié)果:無(wú)成員變量對(duì)象應(yīng)該分配內(nèi)存:isa的8字節(jié)(isa后續(xù)會(huì)進(jìn)行詳細(xì)探索)
實(shí)際打印結(jié)果 : 
申請(qǐng)內(nèi)存大小為:8——-系統(tǒng)開(kāi)辟內(nèi)存大小為:16

class_getInstanceSize 內(nèi)部實(shí)現(xiàn)如下:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize(); // 直接返回字節(jié)對(duì)齊大小
}

回憶上一篇文章, alloc 初始化往下探索其中有一個(gè)方法_class_createInstanceFromZone,內(nèi)部會(huì)調(diào)用size_t size = cls->instanceSize(extraBytes),具體實(shí)現(xiàn)如下:

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

我們通過(guò)查看OC源碼可知,class_getInstanceSize返回類(lèi)的成員變量占據(jù)內(nèi)存的大小,而malloc_size獲取obj指針指向內(nèi)存的大小。
分析得出:系統(tǒng)在創(chuàng)建一個(gè)對(duì)象的時(shí)候,對(duì)象的isa指針占據(jù)8個(gè)字節(jié),但是系統(tǒng)會(huì)為其分配最少16字節(jié)的內(nèi)存空間,所以如果該對(duì)象沒(méi)有成員變量, class_getInstanceSize 會(huì)輸出 8 個(gè)字節(jié),malloc_size 會(huì)輸出最少 16 個(gè)字節(jié)。

2.2 添加成員變量后對(duì)象內(nèi)存探索

DZTeacher.h文件

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@interface DZTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;

main.m文件

#import <Foundation/Foundation.h>
#import "DZTeacher.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZTeacher  *p = [DZTeacher alloc];
        // isa ---- 8
        p.name = @"Dezi";   // sizeof(p.name) = 8
        p.age  = 18;            // 4
        p.height = 185;         // 8
        p.hobby  = @"女";       // 8
        NSLog(@"%@",p);
        NSLog(@"申請(qǐng)內(nèi)存大小為:%lu——-系統(tǒng)開(kāi)辟內(nèi)存大小為:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
    }
    return 0;
}
分析結(jié)果:
1. 成員變量應(yīng)該分配內(nèi)存:8字節(jié) + 4字節(jié) + 8字節(jié) + 8字節(jié) = 28字節(jié)
2. 再加上isa的8字節(jié) = 36字節(jié)(isa后續(xù)會(huì)進(jìn)行詳細(xì)探索)
3. 根據(jù)字節(jié)對(duì)齊原則最大8字節(jié),所以36往大走補(bǔ)充到40字節(jié)
4. 所以?xún)?nèi)存空間應(yīng)該分配40字節(jié)
5. 注意:對(duì)象開(kāi)辟空間的時(shí)候成員變量就會(huì)編譯進(jìn)來(lái),所以成員變量未賦值也會(huì)分配內(nèi)存

實(shí)際打印結(jié)果 : 
申請(qǐng)內(nèi)存大小為:40——-系統(tǒng)開(kāi)辟內(nèi)存大小為:48

根據(jù)上方打印信息,我們的分析是對(duì)的,類(lèi)對(duì)象至少需要40字節(jié),那為什么實(shí)際分配內(nèi)存大小為48字節(jié)呢?下來(lái)我們繼續(xù)探索。

2.2 calloc探索

經(jīng)過(guò)對(duì)源碼的一步步探索,我們發(fā)現(xiàn),在obj = (id)calloc(1, size);這個(gè)方法的時(shí)候,對(duì)象內(nèi)存大小發(fā)生了變化

calloc方法探索

malloc.png

根據(jù)上圖我們發(fā)現(xiàn)calloc方法在malloc源碼里邊,那我們打開(kāi)新的源碼繼續(xù)分析:

DZCallocTest.png

之后進(jìn)入calloc流程,首先調(diào)用malloc_zone_calloc方法

calloc.png

在其內(nèi)部調(diào)用zone->calloc初始化并且返回了一個(gè)ptr指針

malloc_zone_calloc.png

斷點(diǎn)在此處我們發(fā)現(xiàn)遞歸了,那我們?cè)谶@里打印zone->calloc,我們找到malloc.c文件249行的default_zone_calloc方法

zone->calloc.png

加上斷點(diǎn)我們發(fā)現(xiàn)此處又是zone->calloc,繼續(xù)打印zone->calloc發(fā)現(xiàn)nano_malloc.c文件中878行的nano_calloc方法

default_zone_calloc.png

在malloc的源碼中搜索nano_calloc,于nano_calloc.c文件中找到該方法,其中的核心代碼_nano_malloc_check_clear進(jìn)行內(nèi)存申請(qǐng),并且返回一個(gè)指針p

zone->calloc.png

_nano_malloc_check_clear內(nèi)部發(fā)現(xiàn)segregated_size_to_fit方法輸入的size是40,輸出的是48,所以這個(gè)方法是開(kāi)辟內(nèi)存的算法

_nano_malloc_check_clear.png

對(duì)segregated_size_to_fit方法進(jìn)行分析,發(fā)現(xiàn)對(duì)齊原則是16字節(jié)對(duì)齊,所以輸入實(shí)際需要的40,經(jīng)過(guò)16字節(jié)對(duì)齊后輸出的為48

segregated_size_to_fit.png
nano_zone_common.png

2.3 總結(jié)+流程圖

對(duì)象內(nèi)存大小的申請(qǐng)是按照8字節(jié)對(duì)齊,不滿16字節(jié)時(shí)按照16字節(jié)計(jì)算;若大于16字節(jié)時(shí),calloc實(shí)際開(kāi)辟內(nèi)存則是按照16字節(jié)對(duì)齊。
感覺(jué)8字節(jié)對(duì)齊是為了屬性之間內(nèi)存安全提高容錯(cuò)空間,16字節(jié)對(duì)齊是為了保證對(duì)象之間內(nèi)存安全提高容錯(cuò)空間。

calloc.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前話: 在了解內(nèi)存對(duì)齊之前先了解一下各數(shù)據(jù)類(lèi)型在內(nèi)存中的大小,目前我們比較常用的是64位系統(tǒng),所以我們的研究對(duì)象統(tǒng)...
    sz_藍(lán)天使者閱讀 794評(píng)論 0 3
  • 前言 iOS底層探索之對(duì)象原理(一)中了解到通過(guò)calloc我們對(duì)象有了內(nèi)存地址,通過(guò)initInstanceIs...
    litongde閱讀 322評(píng)論 0 2
  • iOS底層探索 - 內(nèi)存補(bǔ)齊 在上篇文章中我們主要探索了對(duì)象的初始化以及怎么開(kāi)辟內(nèi)存。內(nèi)存對(duì)齊三大原則是什么?對(duì)象...
    leng_li閱讀 358評(píng)論 0 0
  • 作者:李振君 這個(gè)孤僻的泥瓦匠 只會(huì)把沉默砌成墻 什么也不說(shuō); 他唯一的愛(ài)好就是 收工后 用滿腹的心事 讓一個(gè)酒瓶...
    李振君閱讀 253評(píng)論 0 4
  • 今天爸爸和媽媽都上班,只有我和姐姐在家里。 早上我們醒來(lái)的時(shí)候。我和姐姐就泡了方便面,吃完了之...
    王啟萱閱讀 689評(píng)論 0 0

友情鏈接更多精彩內(nèi)容