OC對(duì)象的內(nèi)存分配及內(nèi)存對(duì)齊

對(duì)象是如何分配內(nèi)存的?對(duì)象是如何計(jì)算內(nèi)存大小的呢?對(duì)象內(nèi)存分配跟什么有關(guān)?

代碼分析

sizeof() 計(jì)算一個(gè)變量或者類型的大?。ㄒ宰止?jié)為單位)
class_getInstanceSize 計(jì)算對(duì)象所需要的內(nèi)存大小,結(jié)算結(jié)果遵循8字節(jié)對(duì)齊,其實(shí)現(xiàn)源碼如下:

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

malloc_size 計(jì)算對(duì)象的實(shí)際大小,使用時(shí)需要引入頭文件

//沒有添加任何屬性
@interface LNPerson : NSObject
@end
@implementation LNPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      Person *person = [LNPerson alloc];
      NSLog(@"%lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
    }
    return 0;
}

打印結(jié)果:8 - 8 - 16

//添加一個(gè)屬性
@interface LNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LNPerson
@end

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

        LNPerson *person = [LNPerson alloc];
        NSLog(@"打印結(jié)果: %lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
    }
    return 0;
}

打印結(jié)果: 8 - 16 - 16

//添加兩個(gè)屬性
@interface LNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end

@implementation LNPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LNPerson *person = [LNPerson alloc];
        NSLog(@"打印結(jié)果: %lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
    }
    return 0;
}

打印結(jié)果: 8 - 24 - 32
這里先附上一張數(shù)據(jù)類型占用內(nèi)存大小的表格:

圖1.1 數(shù)據(jù)類型占用內(nèi)存大小.png

由以上三分代碼運(yùn)行結(jié)果可以知道對(duì)象的內(nèi)存大小跟屬性有關(guān)(實(shí)際上是跟實(shí)例變量有關(guān))。代碼分析如下:

  • 這里面sizeof的結(jié)果一直沒變化,實(shí)際上他只是計(jì)算person這個(gè)指針的大小,而這個(gè)指針是一直不變的,在OC里面對(duì)象指針的大小為8字節(jié)(其他數(shù)據(jù)類型占用內(nèi)存大小可以參考圖1-1)。
  • 通過class_getInstanceSize打印結(jié)果可以看出隨著屬性的增多,開辟一個(gè)對(duì)象所需要的內(nèi)存也在增加(這里面需要注意的是屬性age是int屬性,如果按照對(duì)象所需要內(nèi)存大小計(jì)算應(yīng)該是8+8(name屬性大小)+ 4(age)= 20,可是為什么結(jié)果卻是24呢?實(shí)際上這是底層結(jié)構(gòu)體的內(nèi)存對(duì)齊遵循了8字節(jié)對(duì)齊的計(jì)算結(jié)果,關(guān)于結(jié)構(gòu)體內(nèi)存對(duì)齊可以參考相關(guān)資料。);
  • malloc_size計(jì)算的是對(duì)象的實(shí)際內(nèi)存大小,我們看到,這跟class_getInstanceSize計(jì)算出來的大小不一致,這實(shí)際上是因?yàn)镺C對(duì)象內(nèi)存分配時(shí)遵循了16字節(jié)對(duì)齊原則,當(dāng)對(duì)象所需內(nèi)存小于16字節(jié)時(shí),還是會(huì)分配16字節(jié);當(dāng)對(duì)象所需內(nèi)存大于16字節(jié)且大小剛好為16的倍數(shù)時(shí),則按照所需大小分配;但是當(dāng)對(duì)象所需內(nèi)存大于16字節(jié)且大小不是16的倍數(shù)時(shí),則分配大于所需內(nèi)存大小的最小的16的倍數(shù)。

什么是內(nèi)存對(duì)齊

內(nèi)存對(duì)齊”應(yīng)該是編譯器的“管轄范圍”。編譯器為程序中的每個(gè)“數(shù)據(jù)單元”安排在適當(dāng)?shù)奈恢蒙?。?nèi)存對(duì)齊意味將數(shù)據(jù)類型寫入到內(nèi)存地址時(shí)是按照它們大小切割的。簡(jiǎn)單說就是如果內(nèi)存地址是n字節(jié)的倍數(shù),那么我們說這n字節(jié)是內(nèi)存對(duì)齊的,注意,這里n是2的冪,說白了,內(nèi)存地址正好放下n字節(jié)的倍數(shù),兩者相除余數(shù)為零,正好整除。例如,從上面的代碼分析可以看出對(duì)象person所需要的實(shí)際大小為24字節(jié),而實(shí)際大小確是32字節(jié),這是因?yàn)镺C對(duì)象的內(nèi)存是16字節(jié)對(duì)齊的。

為什么需要內(nèi)存對(duì)齊

那編譯器為什么要進(jìn)行內(nèi)存對(duì)齊呢?主要基于以下兩個(gè)原因:

  • 1、平臺(tái)原因(移植原因):
    不是所有的硬件平臺(tái)都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
  • 2、性能原因
    數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于,為了訪問未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對(duì)齊的內(nèi)存訪問僅需要一次訪問。

內(nèi)存對(duì)齊遵循什么樣的原則

1、數(shù)據(jù)成員對(duì)齊規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大?。ㄖ灰蓡T有子成員,比如說數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(比如int為4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ)。min(當(dāng)前開始的位置m n))m = 9, n= 4:9 10 11 12 ,12是4的整數(shù)倍,所以從12開始存儲(chǔ)
2、結(jié)構(gòu)體作為成員:如果一個(gè)結(jié)構(gòu)體里有些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素的整數(shù)倍地址開始存儲(chǔ)。(struct a里面存儲(chǔ)struct b, b里有char、int、 double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲(chǔ)。)
3、收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足的要補(bǔ)齊。
舉個(gè)例子:

// 64位
struct MyStruct1 {
    double a;//8字節(jié) a存儲(chǔ)為0~7位置共8位
    char b;//1字節(jié) 8是1的整數(shù)倍,所以b從8位置開始存儲(chǔ),共1位
    int c;// 4字節(jié) 前8位已被占用,9不是4的倍數(shù),所以得往前找最近的4的倍數(shù)12,從12位開始存儲(chǔ),共四位12~15
    short d;//2字節(jié) 16位剛好是2的倍數(shù),從16位開始存儲(chǔ),共兩位16~17,可以推出struct1的所需大小為0~17共18位
}struct1;// 但是結(jié)構(gòu)大小是其內(nèi)部最大成員(這里的a,8字節(jié))的整數(shù)倍,因此最小為24

struct MyStruct2 {// 同上面的推理
    double a;//8 0~7
    int c;// 4 8是4的倍數(shù) 8~11
    char b;//1 12
    short d;//2 13不是2的整數(shù)倍, 故14~15,所需大小位0~15共16位,實(shí)際大小應(yīng)該為16
}struct2;

struct MyStruct3 {// 同上面的推理
    double a;//8 0~7
    int c;// 4 8是4的倍數(shù) 8~11
    char b;//1 12
    short d;//2 13不是2的整數(shù)倍, 故14~15,所需大小15
//如果一個(gè)結(jié)構(gòu)體里有些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素的整數(shù)倍地址開始存儲(chǔ)。所以雖然這里myStruct2的大小為16字節(jié),但是依然以8字節(jié)為倍數(shù)開始計(jì)算,這里16剛好是8的整數(shù)倍,所以myStruct2從16位開始,大小16位,所以應(yīng)該是16~31位,總共大小32位
    struct MyStruct2 myStruct2;//16
}struct3;// 32

打印這三個(gè)結(jié)構(gòu)體:

    NSLog(@"struct1大?。?ld",sizeof(struct1));
    NSLog(@"struct2大?。?ld",sizeof(struct2));
    NSLog(@"struct3大?。?ld",sizeof(struct3));

打印結(jié)果:

2021-07-25 17:12:57.383194+0800 MemoryAlignmentTest[8346:636413] struct1大?。?4
2021-07-25 17:12:57.383265+0800 MemoryAlignmentTest[8346:636413] struct2大?。?6
2021-07-25 17:12:57.383295+0800 MemoryAlignmentTest[8346:636413] struct3大小:32

結(jié)果是符合規(guī)則的。

備注:OC對(duì)象的底層結(jié)構(gòu)是結(jié)構(gòu)體,結(jié)構(gòu)體大小內(nèi)存分配遵循8字節(jié)對(duì)齊原則。但是OC對(duì)象在結(jié)構(gòu)8字節(jié)對(duì)齊計(jì)算出的內(nèi)存大小的基礎(chǔ)上遵循16字節(jié)對(duì)齊原則。這么做應(yīng)該是出于兼容性、容錯(cuò)的等方面的考慮,給對(duì)象留點(diǎn)空間,避免擠滿。

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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