前言
- 在iOS底層源碼學(xué)習(xí)中劲赠,會(huì)需要分析一個(gè)結(jié)構(gòu)體所占用的內(nèi)存大小,這里面就涉及到了內(nèi)存對(duì)齊
- 今天秸谢,我將結(jié)合內(nèi)存對(duì)齊的概念凛澎、原因、規(guī)則估蹄、實(shí)際例子塑煎,讓你深入理解內(nèi)存對(duì)齊,掌握分析結(jié)構(gòu)體所占內(nèi)存大小的方法臭蚁。
目錄.png
源碼地址
簡(jiǎn)介
內(nèi)存對(duì)齊”應(yīng)該是編譯器的“管轄范圍”最铁。編譯器為程序中的每個(gè)“數(shù)據(jù)單元”安排在適當(dāng)?shù)奈恢蒙稀H绻阆肓私飧拥讓拥拿孛芸宥遥骄俊皟?nèi)存對(duì)齊”對(duì)你就不應(yīng)該再模糊了冷尉。
- 平臺(tái)原因(移植原因):不是所有的硬件平臺(tái)都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù)甥角,否則拋出硬件異常网严。
- 性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于嗤无,為了訪問未對(duì)齊的內(nèi)存震束,處理器需要作兩次內(nèi)存訪問;而對(duì)齊的內(nèi)存訪問僅需要一次訪問当犯。
特別對(duì)于我們學(xué)習(xí)底層源碼垢村,是需要掌握的知識(shí)點(diǎn)之一,下面我就結(jié)合百度百科-內(nèi)存對(duì)齊以及實(shí)際的demo進(jìn)行詳細(xì)分析嚎卫。
1嘉栓、規(guī)則定義
規(guī)則1、數(shù)據(jù)成員對(duì)齊規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員拓诸,第一個(gè)數(shù)據(jù)成員放在offset為0的地方侵佃,以后每個(gè)數(shù)據(jù)成員的對(duì)齊,按照#pragma pack指定的數(shù)值和這個(gè)數(shù)據(jù)成員自身長(zhǎng)度中奠支,比較小的那個(gè)進(jìn)行馋辈。
規(guī)則2、結(jié)構(gòu)(或聯(lián)合)的整體對(duì)齊規(guī)則:在數(shù)據(jù)成員完成各自對(duì)齊之后倍谜,結(jié)構(gòu)(或聯(lián)合)本身也要進(jìn)行對(duì)齊迈螟,對(duì)齊將按照#pragma pack指定的數(shù)值和結(jié)構(gòu)(或聯(lián)合)最大數(shù)據(jù)成員長(zhǎng)度中叉抡,比較小的那個(gè)進(jìn)行。
規(guī)則3答毫、結(jié)合1褥民、2可推斷:當(dāng)#pragma pack的n值等于或超過所有數(shù)據(jù)成員長(zhǎng)度的時(shí)候,這個(gè)n值的大小將不產(chǎn)生任何效果洗搂。
2消返、規(guī)則解析
- 規(guī)則1中表明數(shù)據(jù)成員的存放是按照定義的順序依次存放的
- #pragma pack是對(duì)齊系數(shù),每個(gè)平臺(tái)不一樣蚕脏,程序員可以通過預(yù)編譯命令#pragma pack(n)侦副,n=1,2,4,8,16來改變這一系數(shù)(32位平臺(tái)一般為4,64位平臺(tái)一般為8)驼鞭。iOS下默認(rèn)為8。這個(gè)數(shù)值大家可以通過調(diào)試#pragma pack(n)測(cè)試驗(yàn)證得到尺碰。
- 規(guī)則1挣棕,當(dāng)?shù)趚(x>1)個(gè)成員y存放的時(shí)候,y按照min(n,m)來對(duì)齊存放亲桥,其中n為對(duì)齊系數(shù)洛心,m為成員y的數(shù)據(jù)類型長(zhǎng)度。
- 在完成各個(gè)數(shù)據(jù)成員的存放排列后题篷,通過規(guī)則2词身,取min(n,maxM)進(jìn)行對(duì)齊,其中n為對(duì)齊系數(shù)番枚,maxM為所有數(shù)據(jù)成員類型中長(zhǎng)度的最大值法严。
3、實(shí)例分析
demo例子中我們定義具有相同類型和個(gè)數(shù)成員的結(jié)構(gòu)體葫笼,但是其定義的順序不同成員不同深啤,并對(duì)其進(jìn)行賦值,然后結(jié)合規(guī)則路星,詳細(xì)進(jìn)行分析溯街。
基礎(chǔ)知識(shí)點(diǎn)介紹
1.一個(gè)字節(jié)包含8個(gè)二進(jìn)制位
2.一個(gè)十六進(jìn)制位可用4個(gè)二進(jìn)制位表示
3.一個(gè)字節(jié)可以由2個(gè)十六進(jìn)制位表示
0x0000 0000 0000 0008表示16個(gè)16進(jìn)制位,可以表示8個(gè)字節(jié)
所以8可以用8個(gè)字節(jié)0x0000 0000 0000 0008表示洋丐,或者4個(gè)字節(jié)0x0000 0008呈昔,或者2個(gè)字節(jié)0x0008,取決于定義8的數(shù)據(jù)類型友绝。
字符'a'換成ASCII碼為97堤尾,可以用 0x61表示。
此外九榔,iOS系統(tǒng)的編譯平臺(tái)是按照小端法進(jìn)行編譯哀峻。
下面進(jìn)入具體的實(shí)例分析涡相,
環(huán)境:
Xcode 11.3.1,Deployment Target:10.15
代碼如下:
CommandLineTool類型工程的main.m文件中
#import <Foundation/Foundation.h>
struct Person1 {
char a;
long b;
int c;
short d;
}MyPerson1;
struct Person2 {
long b;
char a;
int c;
short d;
}MyPerson2;
struct Person3 {
long b;
int c;
char a;
short d;
}MyPerson3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyPerson1.a = 'a';
MyPerson1.b = 8;
MyPerson1.c = 4;
MyPerson1.d = 2;
MyPerson2.b = 18;
MyPerson2.a = 'a';
MyPerson2.c = 14;
MyPerson2.d = 12;
MyPerson3.b = 28;
MyPerson3.c = 24;
MyPerson3.a = 'a';
MyPerson3.d = 22;
NSLog(@"Adress=======MyPerson1:%p,MyPerson2:%p,MyPerson3:%p",&MyPerson1,&MyPerson2,&MyPerson3);
NSLog(@"Size=======MyPerson1:%lu,MyPerson2:%lu,MyPerson3:%lu",sizeof(MyPerson1),sizeof(MyPerson2),sizeof(MyPerson3));
}
return 0;
}
分析MyPerson1
struct Person1 {
char a;
long b;
int c;
short d;
}MyPerson1;
第一個(gè)成員char類型的成員a='a'占用1字節(jié),此時(shí):
a: 0x61第二個(gè)成員long類型的成員b=8占用8個(gè)字節(jié)剩蟀,根據(jù)規(guī)則解析3催蝗,b=8按照min(8,8)=8對(duì)齊,b的起始位置為8的倍數(shù)育特,不滿足丙号,a需要補(bǔ)齊7個(gè)字節(jié)保證b的起始位置為8的倍數(shù)
此時(shí):
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008第三個(gè)成員int類型的成員c=4占用4個(gè)字節(jié),根據(jù)規(guī)則解析3,整數(shù)c=4需要按照min(8,4)=4進(jìn)行對(duì)齊缰冤,c的起始位置需要為4的整數(shù)倍犬缨,現(xiàn)在已經(jīng)滿足
此時(shí):
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008
c:0x0000 0004第四個(gè)成員short類型的整數(shù)d=2占用2個(gè)字節(jié),根據(jù)規(guī)則解析3棉浸,d按照min(8,2)=2進(jìn)行對(duì)齊怀薛,d的起始位置需要為2的整數(shù)倍,現(xiàn)在已經(jīng)滿足
此時(shí):
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008
c:0x0000 0004
d:0x0002根據(jù)規(guī)則解析4迷郑,結(jié)構(gòu)體需要進(jìn)行整體對(duì)齊枝恋,取min(n,maxDataLength) = max(8,8) = 8對(duì)齊,現(xiàn)在為8+8+4+2=22字節(jié)嗡害,需要補(bǔ)2個(gè)字節(jié)焚碌,按照排列順序,在d占用內(nèi)存段補(bǔ)2個(gè)字節(jié);
最后得到
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008
c:0x0000 0004
d:0000 0002
其中我們看把c和d看成共占用一段8字節(jié)的內(nèi)存霸妹,因?yàn)閷?duì)齊系數(shù)為8十电,編譯器按照8的整數(shù)倍來讀取內(nèi)存地址。按照小端法進(jìn)行修正叹螟,此時(shí)內(nèi)存排列應(yīng)該內(nèi)應(yīng)該是
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008
dc:0x0000 0002 0000 0004鹃骂,
其中dc:0x0000 0002 0000 0004的第1-8位表示成員d的值,右邊第9-16位表示成員c的值綜上,MyPerson1結(jié)構(gòu)體整體占用8+8+8=24字節(jié)
分析MyPerson2
struct Person2 {
long b;
char a;
int c;
short d;
}MyPerson2;
第一個(gè)成員long類型的成員b=18占用8字節(jié)首妖,此時(shí):
b:0x0000 0000 0000 0012第二個(gè)成員char類型的成員a='b'占用1個(gè)字節(jié)偎漫,根據(jù)規(guī)則解析3,a按照min(8,1)=1對(duì)齊有缆,a的起始位置需要為1的整數(shù)倍象踊,已經(jīng)滿足,此時(shí):
b:0x0000 0000 0000 0012
a:0x62第三個(gè)成員int類型的成員c=14占用4個(gè)字節(jié),根據(jù)規(guī)則解析3棚壁,c按照min(8,4)=4進(jìn)行對(duì)齊杯矩,c的起始位置需要未4的整數(shù)倍,不滿足袖外,所以成員a='b'需要補(bǔ)齊3個(gè)字節(jié)史隆, 此時(shí):
b:0x0000 0000 0000 0012
a:0x0000 0062
c:0x0000 000e第四個(gè)成員short類型的成員d=12占用2個(gè)字節(jié),根據(jù)規(guī)則解析3曼验,成員d按照min(8,2)=2進(jìn)行對(duì)齊,起始位置需要為2的整數(shù)倍泌射,已經(jīng)滿足粘姜,此時(shí) :
b:0x0000 0000 0000 0012
a:0x0000 0062
c:0x0000 000e
d:000c根據(jù)規(guī)則解析4,結(jié)構(gòu)體需要進(jìn)行整體對(duì)齊熔酷,取min(n,maxDataLength) = max(8,8) = 8對(duì)齊孤紧, 現(xiàn)在占用8+4+4+2=18個(gè)字節(jié),需要補(bǔ)6個(gè)字節(jié)拒秘,按照排列順序号显,在d占用的內(nèi)存段補(bǔ)6個(gè)字節(jié)
最后得到 :
b:0x0000 0000 0000 0012
a:0x0000 0062
c:0x0000 000e
d:0000 0000 0000 000c,其中我們看把a(bǔ)躺酒、c看成共占用一段8字節(jié)的內(nèi)存押蚤,因?yàn)閷?duì)齊系數(shù)為8,編譯器按照8的整數(shù)倍來讀取內(nèi)存地址按照小端法修正羹应,此時(shí)真正的內(nèi)存排列應(yīng)該內(nèi)應(yīng)該是:
b:0x0000 0000 0000 0012
ca:0x0000 000e 0000 0062
d:0x0000 0000 0000 000c 揽碘,
其中ca:0x0x0000 000e 0000 0062 的第1-8位表示c的值,第9-16表示a的值綜上园匹,MyPerson2整體占用8+8+8=24個(gè)字節(jié)
分析MyPerson3
struct Person3 {
long b;
int c;
char a;
short d;
}MyPerson3;
第一個(gè)成員long類型的成員b=28占用8字節(jié)钾菊,此時(shí):
b:0x0000 0000 0000 001c第二個(gè)成員int類型的成員c=24占用4個(gè)字節(jié),根據(jù)規(guī)則1, 成員c按min(8,4)=4對(duì)齊偎肃,c的起始位置需要為4的整數(shù)倍,已經(jīng)滿足浑此,此時(shí):
b:0x0000 0000 0000 001c
c:0x0000 0018第三個(gè)成員char類型的成員a='c'占用1個(gè)字節(jié),根據(jù)規(guī)則1累颂,成員a按min(8,1)=1進(jìn)行對(duì)齊此時(shí):
b:0x0000 0000 0000 001c
c:0x0000 0018
a:0x63第四個(gè)成員short類型的成員d=22占用2個(gè)字節(jié),根據(jù)規(guī)則1凛俱,成員d按照min(8,2)=2進(jìn)行對(duì)齊,d的起始位置需要為2的整數(shù)倍紊馏,因此成員a需要補(bǔ)1字節(jié),此時(shí):
b:0x0000 0000 0000 001c
c:0x0000 0018
a:0x0063
d:0x0016根據(jù)規(guī)則解析4蒲犬,結(jié)構(gòu)體需要進(jìn)行整體對(duì)齊朱监,取min(n,maxDataLength) = max(8,8) = 8對(duì)齊,但是現(xiàn)在占用8+4+2+2=16個(gè)字節(jié)原叮,已經(jīng)滿足了
最后得到:
b:0x0000 0000 0000 001c
c:0x0000 0018
a:0063
d:0016赫编,其中我們看把c、a奋隶、d看成共占用一段8字節(jié)的內(nèi)存擂送,因?yàn)閷?duì)齊系數(shù)為8,編譯器按照8的整數(shù)倍來讀取內(nèi)存地址按照小端法修正唯欣,此時(shí)真正的內(nèi)存排列應(yīng)該內(nèi)應(yīng)該是 最后得到
b:0x0000 0000 0000 001c
dac:0x0016 0063 0000 0018 ,其中dac:0x0016 0063 0000 0018的左邊第1-4位表示d存儲(chǔ)的值嘹吨,左邊第5-8位表示a存儲(chǔ)的值,右邊第9-16位表示c存儲(chǔ)的值
綜上境氢,MyPerson3結(jié)構(gòu)體整體占用8+8=16個(gè)字節(jié)
驗(yàn)證
如圖輸出結(jié)構(gòu)體成員信息
- 我們把各個(gè)結(jié)構(gòu)體的地址打印出來蟀拷,然后利用lldb的x/4gx命令輸出各個(gè)結(jié)構(gòu)體里面的從第一個(gè)成員的起始位置開始的4段8字節(jié)內(nèi)存信息
- x/4gx 0x100002020表示打印從MyPerson1的成員a='a'開始的4段內(nèi)存信息碰纬,其中前3段 0x0000000000000061 0x0000000000000008,0x0000000200000004和我們前面分析的MyPerson1得出的內(nèi)存表示一致问芬,最后一段0x0000000000000012不屬于MyPerson1悦析,代表MyPerson2的成員b=18內(nèi)存表示
- x/4gx 0x100002038表示打印從MyPerson2的成員b=18開始的4段內(nèi)存信息,其中前3段 0x0000000000000012愈诚,0x0000000e00000062, 0x000000000000000c和我們前面分析的MyPerson2得出的內(nèi)存表示一致她按,最后一段0x000000000000001c不屬于MyPerson2,代表MyPerson3的成員b=28的內(nèi)存表示
- x/4gx 0x100002050 表示打印從MyPerson3的成員b=28開始的4段內(nèi)存信息炕柔,其中前2段 0x000000000000001c酌泰,0x0016006300000018和我們前面分析的MyPerson2得出的內(nèi)存表示一致,后面2段0x0000000000000000不屬于MyPerson3
優(yōu)化lldb的打印輸出如下
- 通過優(yōu)化輸出可以看到lldb輸出的內(nèi)存表示與我們前面實(shí)例分析的是一致匕累。
OC對(duì)象分析
仿照上面的3個(gè)結(jié)構(gòu)體定義3個(gè)類Teacher1,Teacher2,Teacher3
@interface Teacher1 : NSObject
@property (nonatomic, assign) char a;
@property (nonatomic, assign) long b;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;
@end
@interface Teacher2 : NSObject
@property (nonatomic, assign) long b;
@property (nonatomic, assign) char a;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;
@end
@interface Teacher3 : NSObject
@property (nonatomic, assign) int c;
@property (nonatomic, assign) long b;
@property (nonatomic, assign) char a;
@property (nonatomic, assign) short d;
@end
main.m中添加如下代碼
Teacher1 *t1 = [[Teacher1 alloc] init];
t1.a = 'a';
t1.b = 8;
t1.c = 4;
t1.d = 2;
Teacher2 *t2 = [[Teacher2 alloc] init];
t2.b = 18;
t2.a = 'b';
t2.c = 14;
t2.d = 12;
Teacher3 *t3 = [[Teacher3 alloc] init];
t3.b = 28;
t3.c = 24;
t3.a = 'c';
t3.d = 22;
對(duì)象的輸出如下
- 可以看到陵刹,3個(gè)對(duì)象的第2個(gè)八字節(jié)和第三個(gè)八字節(jié)這2個(gè)內(nèi)存段存儲(chǔ)了我們定義的成員a、b欢嘿、c衰琐、d(準(zhǔn)確表述為_a,_b炼蹦,_c羡宙,_d)的值,說明編譯器做了相應(yīng)的優(yōu)化掐隐,不會(huì)直接按照我們?cè)陬愔卸x成員的順序生成構(gòu)造對(duì)應(yīng)的結(jié)構(gòu)體
- 3個(gè)對(duì)象的第一個(gè)八字節(jié)存儲(chǔ)著各自isa的值
- 如果定義有float或者double類型的成員狗热,比如Teacher1
@interface Teacher1 : NSObject
@property (nonatomic, assign) double height;
@end
Teacher1 *t1 = [[Teacher1 alloc] init];
t1.height = 175;
由于float和double的位表示是經(jīng)過一定算法得到,無法直接通過簡(jiǎn)單手工計(jì)算得出虑省,可以使用lldb命令:p/x (double)175得到的其位表示匿刮,再與x/4gx t1中的進(jìn)行對(duì)比。
4探颈、思考
下面代碼輸出什么熟丸?
NSLog(@"sizeof=======t1:%lu,t2:%lu,t3:%lu",sizeof(t1),sizeof(t2),sizeof(t3));
NSLog(@"class_getInstanceSize=======t1:%lu,t2:%lu,t3:%lu",class_getInstanceSize(t1.class),class_getInstanceSize(t2.class),class_getInstanceSize(t3.class));
NSLog(@"malloc_size=======t1:%lu,t2:%lu,t3:%lu",malloc_size((__bridge const void*)t1),malloc_size((__bridge const void*)t2),malloc_size((__bridge const void*)t3));
輸出如下:
sizeof=======t1:8,t2:8,t3:8
class_getInstanceSize=======t1:24,t2:24,t3:24
malloc_size=======t1:32,t2:32,t3:32
- sizeof計(jì)算的是傳入?yún)?shù)的類型所占字節(jié)大小。t1為Teacher1 *類型伪节,本質(zhì)上是一個(gè)objc_objct的結(jié)構(gòu)體指針光羞,所以占8字節(jié),t2架馋、t3同理狞山。
- class_getInstanceSize輸出的是對(duì)象實(shí)例經(jīng)過內(nèi)存對(duì)齊后的占用的大小。前面通過分析叉寂,t1所占大小應(yīng)該為16字節(jié)萍启,但是t1本質(zhì)上是一個(gè)objc_objct的結(jié)構(gòu)體指針,objc_objct結(jié)構(gòu)體內(nèi)部還有一個(gè)占8字節(jié)的isa_t,即我們通過lldb調(diào)試x/4gx輸出的第一段內(nèi)存,因此為8+16=24字節(jié)勘纯,t2局服、t3同理。
- malloc_size返回的是指針?biāo)赶虻膬?nèi)存空間所占的大小驳遵,即系統(tǒng)實(shí)際分配的大小淫奔。其值為class_getInstanceSize的值按16字節(jié)對(duì)齊得到,因此為32字節(jié)堤结。
關(guān)于class_getInstanceSize唆迁、malloc_size的詳解參考《OC底層系列二》-對(duì)象中的class_getInstanceSize以及malloc_size部分。
總結(jié)
- 本文主要對(duì)內(nèi)存對(duì)齊的規(guī)則進(jìn)行介紹和并結(jié)合實(shí)際的demo例子對(duì)結(jié)構(gòu)體和對(duì)象進(jìn)行詳細(xì)分析