iOS底層系列06 -- OC對象的內(nèi)存對齊與分配

  • 在闡述OC對象內(nèi)存對齊之前,我們先來看個實(shí)例代碼
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSObject *p1 = [[NSObject alloc]init];
    
        int size1 = sizeof(p1);
        size_t size2 = class_getInstanceSize([p1 class]);
        size_t  size3 = malloc_size((__bridge const void*)(p1));
        
        NSLog(@" size1 = %d",size1);
        NSLog(@" size2 = %lu",size2);
        NSLog(@" size3 = %lu",size3);
    }
    return 0;
}
  • 上述代碼的運(yùn)行結(jié)果如下:
Snip20210624_2.png
  • 同一個對象,獲取對象內(nèi)存大小的三種方式,所獲取的結(jié)果存在差異;
  • sizeof():計算數(shù)據(jù)類型占用的內(nèi)存大小,其參數(shù)可以傳基本數(shù)據(jù)類型、對象類型、指針類型,對于類似于int這樣的基本數(shù)據(jù)而言,sizeof獲取的就是數(shù)據(jù)類型占用的內(nèi)存大小,不同的數(shù)據(jù)類型所占用的內(nèi)存大小是不一樣的;
  • class_getInstanceSize()是用來計算實(shí)例對象 實(shí)際占用的內(nèi)存大小,采用的是8字節(jié)對齊的方式進(jìn)行運(yùn)算的,也就是說對象實(shí)際占用的內(nèi)存大小是8的倍數(shù),其實(shí)現(xiàn)源碼如下所示:
size_t class_getInstanceSize(Class cls){
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
  • 其中 WORD_MASK是宏,其定義為define WORD_MASK 7UL;
  • malloc_size()是用來計算實(shí)例對象分配的內(nèi)存大小,其是由系統(tǒng)完成的,采用的是16字節(jié)對齊的方式進(jìn)行運(yùn)算的,也就是說對象實(shí)際占用的內(nèi)存大小是16的倍數(shù),在iOS底層系列03 -- alloc init new方法的探索文章中已經(jīng)做了非常詳盡的闡述;
  • 在研究OC對象內(nèi)存對齊,我們先來探索結(jié)構(gòu)體的內(nèi)存對齊,因?yàn)镺C對象在底層的本質(zhì)就是結(jié)構(gòu)體;

結(jié)構(gòu)體內(nèi)存對齊

  • 每個特定平臺上的編譯器都有自己的默認(rèn)“對齊系數(shù)”(也叫對齊模數(shù)),我們可以通過預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來修改這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”,在iOS中,Xcode默認(rèn)為#pragma pack(8),即8字節(jié)對齊;

  • 結(jié)構(gòu)體內(nèi)存對齊的規(guī)則:

    • 規(guī)則一: 結(jié)構(gòu)體第一個數(shù)據(jù)成員的起始地址是在結(jié)構(gòu)體內(nèi)存地址偏移量offset=0的位置,然后依次排列其他數(shù)據(jù)成員,其他數(shù)據(jù)成員必須滿足當(dāng)前數(shù)據(jù)成員的起始位置(結(jié)構(gòu)體內(nèi)存地址偏移量offset)是當(dāng)前數(shù)據(jù)成員本身內(nèi)存大小的整數(shù)倍;
    • 規(guī)則二:數(shù)據(jù)成員為結(jié)構(gòu)體:即當(dāng)結(jié)構(gòu)體中嵌套了結(jié)構(gòu)體時,必須滿足作為數(shù)據(jù)成員的結(jié)構(gòu)體的起始位置是其最大成員內(nèi)存大小的整數(shù)倍,比如結(jié)構(gòu)體a嵌套結(jié)構(gòu)體b,b中有char、int、double等,則b的最大成員內(nèi)存大小為8;
    • 規(guī)則三:最后結(jié)構(gòu)體的內(nèi)存大小必須是結(jié)構(gòu)體中最大成員內(nèi)存大小的整數(shù)倍,不足的需要補(bǔ)齊;
  • 第一個代碼實(shí)例:

struct Student{
    int age;
    char name;
    float weight;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct Student s1;
        NSLog(@" s1的內(nèi)存大小 = %lu",sizeof(s1)); //s1的內(nèi)存大小 = 12
    }
    return 0;
}
  • int整型變量age,從offset = 0開始 占4個字節(jié),即占用[0,3];
  • char字符型name,此時offset = 4,是char本身內(nèi)存大小的整數(shù)倍,滿足規(guī)則一,所以占用(3,4];
  • float浮點(diǎn)型weight,此時offset = 5,不是float本身內(nèi)存大小的整數(shù)倍,需要補(bǔ)齊3個字節(jié),此時offset = 8,滿足是float本身內(nèi)存大小的整數(shù)倍,所以占用[8,11];
  • 所有數(shù)據(jù)成員排列完成占用了12個字節(jié),滿足規(guī)則三,最終結(jié)構(gòu)體的內(nèi)存大小為12個字節(jié);
  • 內(nèi)存布局如下:
Snip20210205_61.png
  • 第二個代碼實(shí)例:
struct Dog{
    char name;
    int age;
    double height;
    short color;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct Dog d1;
        NSLog(@"d1的內(nèi)存大小 = %lu",sizeof(d1));//d1的內(nèi)存大小=24
    }
    return 0;
}
  • char字符型name,從offset = 0開始,占用[0,1);
  • int整型變量age,此時offset =1, 不是int本身內(nèi)存大小的整數(shù)倍 需要補(bǔ)齊3個字節(jié),此時offset = 4,所以占用[4,7];
  • double浮點(diǎn)型height,此時offset = 8,滿足是double本身內(nèi)存大小的整數(shù)倍,所以占用[8,15];
  • short字符型color,此時offset =16,滿足是short本身內(nèi)存大小的整數(shù)倍,所以占用[16,17];
  • 所有數(shù)據(jù)成員排列完成占用了18個字節(jié),不滿足規(guī)則三,需補(bǔ)齊4個字節(jié),滿足規(guī)則三,最終結(jié)構(gòu)體占用24個字節(jié);
Snip20210205_62.png
  • 第三個代碼實(shí)例:在第二個實(shí)例的基礎(chǔ)上使用同一個結(jié)構(gòu)體Dog,只不過更改了數(shù)據(jù)成員的順序;
struct Dog{
    double height;
    int age;
    short color;
    char name;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct Dog d1;
        NSLog(@"d1的內(nèi)存大小 = %lu",sizeof(d1));//d1的內(nèi)存大小=16
    }
    return 0;
}
  • double浮點(diǎn)型height,從offset = 0開始,占用[0,7];
  • int整型變量age,此時offset = 8 是int本身內(nèi)存大小的整數(shù)倍 所以占用[8,11];
  • short浮點(diǎn)型color,此時offset = 12,滿足是short本身內(nèi)存大小的整數(shù)倍,所以占用[12,13];
  • char字符型name,此時offset = 14,滿足是char本身內(nèi)存大小的整數(shù)倍,所以占用[14,15);
  • 所有數(shù)據(jù)成員排列完成占用了15個字節(jié),不滿足規(guī)則三,需補(bǔ)齊1個字節(jié),滿足規(guī)則三,最終結(jié)構(gòu)體占用16個字節(jié);
Snip20210205_63.png
  • 可以看出結(jié)構(gòu)體成員的排列順序會影響結(jié)構(gòu)體內(nèi)存的大小,這就為內(nèi)存優(yōu)化--屬性重排提供了理論依據(jù);

  • 第四個代碼實(shí)例:結(jié)構(gòu)體中嵌套結(jié)構(gòu)體

struct Dog{
    double height;
    int age;
    short color;
    char name;
};

struct Student{
    char name;
    double height;
    int age;
    struct Dog dog;
    float weight;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct Student s1;
        NSLog(@"s1的內(nèi)存大小 = %lu",sizeof(s1));//d1的內(nèi)存大小=48
    }
    return 0;
}
  • char字符型name,從offset = 0開始,占用[0,1);
  • double整型變量height,此時offset = 1,不滿足規(guī)則一,需補(bǔ)齊7個字節(jié),此時offset = 8,滿足規(guī)則一,所以占用[8,15];
  • int整型age,此時offset=16,滿足規(guī)則一,所以占用[16,19];
  • struct結(jié)構(gòu)體dog,此時offset = 20,dog結(jié)構(gòu)最大成員內(nèi)存大小為8個字節(jié),那么不滿足規(guī)則二,需補(bǔ)齊4個字節(jié),此時offset = 24,滿足規(guī)則二,結(jié)構(gòu)體dog占用16個字節(jié),所以在Student結(jié)構(gòu)體中占用[24,39];
  • float浮點(diǎn)型weight,此時offset = 40,滿足規(guī)則一,所以占用[40,44];
  • 所有數(shù)據(jù)成員排列完成占用了44個字節(jié),不滿足規(guī)則三,需補(bǔ)齊4個字節(jié),滿足規(guī)則三,最終結(jié)構(gòu)體占用48個字節(jié);

內(nèi)存優(yōu)化(屬性重排)

  • 從上面的結(jié)構(gòu)體內(nèi)存對齊的第二個和第三個實(shí)例代碼分析,得到結(jié)構(gòu)體成員的順序影響結(jié)構(gòu)體內(nèi)存的大??;也可以類推到OC對象;

  • 首先介紹幾個LLDB調(diào)試命令:

    • po 對象 [打印對象信息]
    • x 對象 [讀取對象內(nèi)存信息]
    • x/3gx 對象 [讀取3個以8字節(jié)內(nèi)存信息為單位,以16進(jìn)制形式輸出的內(nèi)存數(shù)據(jù)] (g--8個字節(jié))
    • x/4wx 對象 [讀取4個以4字節(jié)內(nèi)存信息為單位,以16進(jìn)制形式輸出的內(nèi)存數(shù)據(jù)] (w--4個字節(jié))
    • x/8hx 對象 [讀取8個以2字節(jié)內(nèi)存信息為單位,以16進(jìn)制形式輸出的內(nèi)存數(shù)據(jù)] (h--2個字節(jié))
    • x/16bx 對象 [讀取16個以1字節(jié)內(nèi)存信息為單位,以16進(jìn)制形式輸出的內(nèi)存數(shù)據(jù)] (b--1個字節(jié))
    • x/3gx ,x/4wx,x/8hx,x/16bx 讀取的內(nèi)存數(shù)據(jù) 已按小端模式顯示了;
  • 注釋:4個二進(jìn)制位可以完整表示一個16進(jìn)制位所包含的所有數(shù)據(jù),那么兩個16進(jìn)制位可以表示8個二進(jìn)制位,也就是一個字節(jié);

  • 第一個實(shí)例代碼:

@interface YYPerson : NSObject

@property(nonatomic,assign)int age;

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YYPerson *p1 = [[YYPerson alloc]init];
        p1.age = 10;
    }
    return 0;
}
  • 斷點(diǎn)調(diào)試如下:
Snip20210205_67.png
  • po p1打印出p1的內(nèi)存地址;
  • x p1 讀取p1的內(nèi)存信息,我們知道OC對象的isa占用8個字節(jié),所以前面的16個16進(jìn)制位,(每兩個16進(jìn)制位是一個字節(jié))就表示8個字節(jié),又由于iOS是小端模式即低地址存儲低字節(jié)數(shù)據(jù),所以數(shù)據(jù)的讀取從右往左--從高位到低位;
  • age是int類型占用4個字節(jié),下面 po 0x000000000000000a其實(shí)是打印了8個字節(jié)的內(nèi)存數(shù)據(jù),但由于后面四個字節(jié)都是空的,所以最終打印的age的值為10;
  • YYPerson 的內(nèi)存布局為isa + age 其占用(isa 8 + age 4)=12,再進(jìn)行8字節(jié)對齊,最后占用16個字節(jié);
  • 第二個實(shí)例代碼:YYPerson新增字符串屬性name
@interface YYPerson : NSObject

@property(nonatomic,assign)int age;
@property(nonatomic,copy)NSString *name;

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YYPerson *p1 = [[YYPerson alloc]init];
        p1.age = 10;
        p1.name = @"liyanyan";
    }
    return 0;
}
  • 斷點(diǎn)調(diào)試如下:
Snip20210205_68.png
  • YYPerson 的內(nèi)存布局為isa+ age+name 其中isa占8個字節(jié),age占4個字節(jié),name占用8個字節(jié);

  • YYPerson占用(isa 8 + age 4 + name 8)= 20,再進(jìn)行8字節(jié)內(nèi)存對齊,最后占用24個字節(jié);

  • 第三個實(shí)例代碼:YYPerson新增char屬性p_char

@interface YYPerson : NSObject

@property(nonatomic,assign)int age;
@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)char p_char;

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YYPerson *p1 = [[YYPerson alloc]init];
        p1.age = 10;
        p1.name = @"liyanyan";
        p1.p_char = 'A'; //65
    }
    return 0;
}
  • 斷點(diǎn)調(diào)試如下:
Snip20210205_72.png
  • 從調(diào)試控制臺上可以看出YYPerson對象占用24個字節(jié),內(nèi)存布局依次是isa+char+int+NSString,與YYPerson定義的屬性順序并不一致,如果按照定義的屬性順序進(jìn)行內(nèi)存布局,其占用的字節(jié)數(shù)為32個;

  • 從這里就可以看出,系統(tǒng)為了優(yōu)化內(nèi)存,會在每個對象內(nèi)部進(jìn)行屬性重排,并使用8字節(jié)對齊,使單個對象占用的資源盡可能小;

  • 第四個實(shí)例代碼:

@interface YYPerson : NSObject

@property(nonatomic,assign)int age;
@property(nonatomic,copy)NSString *name1;
@property(nonatomic,assign)char p_char1;
@property(nonatomic,copy)NSString *name2;
@property(nonatomic,assign)char p_char2;
@property(nonatomic,assign)short s_age;
@property(nonatomic,assign)char p_char3;
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YYPerson *p1 = [[YYPerson alloc]init];
        p1.age = 10;
        p1.name1 = @"liyanyan";
        p1.p_char1 = 'A'; //65
        p1.name2 = @"li";
        p1.p_char2 = 'B'; //66
        p1.s_age = 30;
        p1.p_char3 = 'C'; //67
        
        int size1 = sizeof(p1);
        size_t size2 = class_getInstanceSize([p1 class]);
        size_t  size3 = malloc_size((__bridge const void*)(p1));
        
        NSLog(@" size1 = %d",size1); //8
        NSLog(@" size2 = %zu",size2); //40
        NSLog(@" size3 = %zu",size3); //48
    }
    return 0;
}
  • 內(nèi)存分析如下:
Snip20210207_75.png
  • x p1 打印出YYPerson對象在內(nèi)存中信息內(nèi)存,從控制臺的結(jié)果來看35 35 00 00 01 80 1d 00這是首8個字節(jié)的內(nèi)容是isa的內(nèi)容,41 42 43 00 1e 00 00 00這是后面緊跟的8個字節(jié)的內(nèi)容,其中前面四個字節(jié)應(yīng)該是三個char類型的屬性值和一個空字節(jié)內(nèi)容,分別對應(yīng)p_char1,p_char2和p_char3,然后 后面四個字節(jié)1e 00 00 00應(yīng)該是short類型s_age的值;

  • 從上面的分析不難得出YYPerson的內(nèi)存布局為:isa+ p_char1+ p_char2 + p_char3 + s_age+age+name1+name2這與YYPerson在.h文件中定義的屬性順序不同,表明系統(tǒng)做了內(nèi)存優(yōu)化,進(jìn)行了屬性的重排;
    -x/5gx p1打印出5個以8字節(jié)為單位的內(nèi)存信息,總共40個字節(jié)的內(nèi)存數(shù)據(jù);po 0x0000000100002010 可以看出是屬性name1的值 p1.name1 = @"liyanyan";po 0x0000000100002030可以看出是屬性name2的值 p1.name2 = @"li";
    0x001d800100003535 -- 是isa的內(nèi)容;
    0x0000001e00434241 -- 包含了三個char/一個空字節(jié)和一個short/兩個空字節(jié)的內(nèi)容;
    0x000000000000000a -- 包含一個int age的內(nèi)容和四個空字節(jié);
    總共40個字節(jié);也就是說YYPerson的內(nèi)存大小為40個字節(jié);

  • x/10wx p1 打印出10個以4字節(jié)為單位的內(nèi)存信息,總共40個字節(jié)的內(nèi)存數(shù)據(jù),其中0x0000000a是int age的值=30,后面四個字節(jié)內(nèi)容0x00000000為空,是因?yàn)閮?nèi)存對齊的原因,空出了這四個字節(jié),p 0x0000000a打印的是age的值p1.age = 10;
    p 0x0000001e打印的是s_age的值p1.s_age = 30;

Snip20210207_76.png
  • x/16bx p1 打印出16個以1字節(jié)為單位的內(nèi)存信息,總共16個字節(jié)的內(nèi)存數(shù)據(jù);

  • p 0x41打印出p_char1= 65 ('A') 占一個字節(jié);

  • p 0x42打印出p_char2= 66 ('B') 占一個字節(jié);

  • p 0x43打印出p_char3= 67 ('C') 占一個字節(jié);

  • 最后由size_t size2 = class_getInstanceSize([p1 class]),計算出YYPerson內(nèi)存大小為40個字節(jié);即size2 = 40;與上面的分析吻合;

  • 但是由malloc_size()函數(shù),計算出來的結(jié)果 size3 = 48,即YYPerson需要分配48個字節(jié)的內(nèi)存空間,與上面size2=40 YYPerson實(shí)際的內(nèi)存大小不匹配,原因在于malloc_size()函數(shù)的內(nèi)部計算邏輯不同,下一章專門探討malloc_size()函數(shù)的底層實(shí)現(xiàn);

最終總結(jié):
  • OC對象在計算實(shí)際占用內(nèi)存大小時采用8字節(jié)對齊,即調(diào)用了class_getInstanceSize()函數(shù)進(jìn)行計算的;
  • OC對象在計算實(shí)際分配內(nèi)存大小采用的16字節(jié)對齊,即調(diào)用了calloc()函數(shù)或者malloc_size()函數(shù);
  • 系統(tǒng)為了優(yōu)化內(nèi)存,會在每個對象內(nèi)部進(jìn)行屬性重排,并使用8字節(jié)對齊,使單個對象占用的內(nèi)存資源盡可能的??;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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