- 在闡述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é)果如下:

- 同一個對象,獲取對象內(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)存布局如下:

- 第二個代碼實(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é);

- 第三個代碼實(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é);

可以看出
結(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)試如下:

-
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)試如下:

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)試如下:

從調(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)存分析如下:

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;

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)存資源盡可能的??;