前言
- 在iOS底层源码学习中,会需要分析一个结构体所占用的内存大小,这里面就涉及到了内存对齐
- 今天,我将结合内存对齐的概念、原因、规则、实际例子,让你深入理解内存对齐,掌握分析结构体所占内存大小的方法。
源码地址
简介
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。如果你想了解更加底层的秘密,探究“内存对齐”对你就不应该再模糊了。
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
特别对于我们学习底层源码,是需要掌握的知识点之一,下面我就结合百度百科-内存对齐以及实际的demo进行详细分析。
1、规则定义
规则1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
规则2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
规则3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
2、规则解析
- 规则1中表明数据成员的存放是按照定义的顺序依次存放的
- #pragma pack是对齐系数,每个平台不一样,程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数(32位平台一般为4,64位平台一般为8)。iOS下默认为8。这个数值大家可以通过调试#pragma pack(n)测试验证得到。
- 规则1,当第x(x>1)个成员y存放的时候,y按照min(n,m)来对齐存放,其中n为对齐系数,m为成员y的数据类型长度。
- 在完成各个数据成员的存放排列后,通过规则2,取min(n,maxM)进行对齐,其中n为对齐系数,maxM为所有数据成员类型中长度的最大值。
3、实例分析
demo例子中我们定义具有相同类型和个数成员的结构体,但是其定义的顺序不同成员不同,并对其进行赋值,然后结合规则,详细进行分析。
基础知识点介绍
1.一个字节包含8个二进制位
2.一个十六进制位可用4个二进制位表示
3.一个字节可以由2个十六进制位表示
0x0000 0000 0000 0008表示16个16进制位,可以表示8个字节
所以8可以用8个字节0x0000 0000 0000 0008表示,或者4个字节0x0000 0008,或者2个字节0x0008,取决于定义8的数据类型。
字符'a'换成ASCII码为97,可以用 0x61表示。
此外,iOS系统的编译平台是按照小端法进行编译。
下面进入具体的实例分析,
环境:
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;
第一个成员char类型的成员a='a'占用1字节,此时:
a: 0x61第二个成员long类型的成员b=8占用8个字节,根据规则解析3,b=8按照min(8,8)=8对齐,b的起始位置为8的倍数,不满足,a需要补齐7个字节保证b的起始位置为8的倍数
此时:
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008第三个成员int类型的成员c=4占用4个字节,根据规则解析3,整数c=4需要按照min(8,4)=4进行对齐,c的起始位置需要为4的整数倍,现在已经满足
此时:
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008
c:0x0000 0004第四个成员short类型的整数d=2占用2个字节,根据规则解析3,d按照min(8,2)=2进行对齐,d的起始位置需要为2的整数倍,现在已经满足
此时:
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008
c:0x0000 0004
d:0x0002根据规则解析4,结构体需要进行整体对齐,取min(n,maxDataLength) = max(8,8) = 8对齐,现在为8+8+4+2=22字节,需要补2个字节,按照排列顺序,在d占用内存段补2个字节;
最后得到
a:0x0000 0000 0000 0061
b:0x0000 0000 0000 0008
c:0x0000 0004
d:0000 0002
其中我们看把c和d看成共占用一段8字节的内存,因为对齐系数为8,编译器按照8的整数倍来读取内存地址。按照小端法进行修正,此时内存排列应该内应该是
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结构体整体占用8+8+8=24字节
分析MyPerson2
struct Person2 {
long b;
char a;
int c;
short d;
}MyPerson2;
第一个成员long类型的成员b=18占用8字节,此时:
b:0x0000 0000 0000 0012第二个成员char类型的成员a='b'占用1个字节,根据规则解析3,a按照min(8,1)=1对齐,a的起始位置需要为1的整数倍,已经满足,此时:
b:0x0000 0000 0000 0012
a:0x62第三个成员int类型的成员c=14占用4个字节,根据规则解析3,c按照min(8,4)=4进行对齐,c的起始位置需要未4的整数倍,不满足,所以成员a='b'需要补齐3个字节, 此时:
b:0x0000 0000 0000 0012
a:0x0000 0062
c:0x0000 000e第四个成员short类型的成员d=12占用2个字节,根据规则解析3,成员d按照min(8,2)=2进行对齐,起始位置需要为2的整数倍,已经满足,此时 :
b:0x0000 0000 0000 0012
a:0x0000 0062
c:0x0000 000e
d:000c根据规则解析4,结构体需要进行整体对齐,取min(n,maxDataLength) = max(8,8) = 8对齐, 现在占用8+4+4+2=18个字节,需要补6个字节,按照排列顺序,在d占用的内存段补6个字节
最后得到 :
b:0x0000 0000 0000 0012
a:0x0000 0062
c:0x0000 000e
d:0000 0000 0000 000c,其中我们看把a、c看成共占用一段8字节的内存,因为对齐系数为8,编译器按照8的整数倍来读取内存地址按照小端法修正,此时真正的内存排列应该内应该是:
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个字节
分析MyPerson3
struct Person3 {
long b;
int c;
char a;
short d;
}MyPerson3;
第一个成员long类型的成员b=28占用8字节,此时:
b:0x0000 0000 0000 001c第二个成员int类型的成员c=24占用4个字节,根据规则1, 成员c按min(8,4)=4对齐,c的起始位置需要为4的整数倍,已经满足,此时:
b:0x0000 0000 0000 001c
c:0x0000 0018第三个成员char类型的成员a='c'占用1个字节,根据规则1,成员a按min(8,1)=1进行对齐此时:
b:0x0000 0000 0000 001c
c:0x0000 0018
a:0x63第四个成员short类型的成员d=22占用2个字节,根据规则1,成员d按照min(8,2)=2进行对齐,d的起始位置需要为2的整数倍,因此成员a需要补1字节,此时:
b:0x0000 0000 0000 001c
c:0x0000 0018
a:0x0063
d:0x0016根据规则解析4,结构体需要进行整体对齐,取min(n,maxDataLength) = max(8,8) = 8对齐,但是现在占用8+4+2+2=16个字节,已经满足了
最后得到:
b:0x0000 0000 0000 001c
c:0x0000 0018
a:0063
d:0016,其中我们看把c、a、d看成共占用一段8字节的内存,因为对齐系数为8,编译器按照8的整数倍来读取内存地址按照小端法修正,此时真正的内存排列应该内应该是 最后得到
b:0x0000 0000 0000 001c
dac:0x0016 0063 0000 0018 ,其中dac:0x0016 0063 0000 0018的左边第1-4位表示d存储的值,左边第5-8位表示a存储的值,右边第9-16位表示c存储的值
综上,MyPerson3结构体整体占用8+8=16个字节
验证
如图输出结构体成员信息
- 我们把各个结构体的地址打印出来,然后利用lldb的x/4gx命令输出各个结构体里面的从第一个成员的起始位置开始的4段8字节内存信息
- x/4gx 0x100002020表示打印从MyPerson1的成员a='a'开始的4段内存信息,其中前3段 0x0000000000000061 0x0000000000000008,0x0000000200000004和我们前面分析的MyPerson1得出的内存表示一致,最后一段0x0000000000000012不属于MyPerson1,代表MyPerson2的成员b=18内存表示
- x/4gx 0x100002038表示打印从MyPerson2的成员b=18开始的4段内存信息,其中前3段 0x0000000000000012,0x0000000e00000062, 0x000000000000000c和我们前面分析的MyPerson2得出的内存表示一致,最后一段0x000000000000001c不属于MyPerson2,代表MyPerson3的成员b=28的内存表示
- x/4gx 0x100002050 表示打印从MyPerson3的成员b=28开始的4段内存信息,其中前2段 0x000000000000001c,0x0016006300000018和我们前面分析的MyPerson2得出的内存表示一致,后面2段0x0000000000000000不属于MyPerson3
优化lldb的打印输出如下
- 通过优化输出可以看到lldb输出的内存表示与我们前面实例分析的是一致。
OC对象分析
仿照上面的3个结构体定义3个类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;
对象的输出如下
- 可以看到,3个对象的第2个八字节和第三个八字节这2个内存段存储了我们定义的成员a、b、c、d(准确表述为_a,_b,_c,_d)的值,说明编译器做了相应的优化,不会直接按照我们在类中定义成员的顺序生成构造对应的结构体
- 3个对象的第一个八字节存储着各自isa的值
- 如果定义有float或者double类型的成员,比如Teacher1
@interface Teacher1 : NSObject
@property (nonatomic, assign) double height;
@end
Teacher1 *t1 = [[Teacher1 alloc] init];
t1.height = 175;
由于float和double的位表示是经过一定算法得到,无法直接通过简单手工计算得出,可以使用lldb命令:p/x (double)175得到的其位表示,再与x/4gx t1中的进行对比。
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计算的是传入参数的类型所占字节大小。t1为Teacher1 *类型,本质上是一个objc_objct的结构体指针,所以占8字节,t2、t3同理。
- class_getInstanceSize输出的是对象实例经过内存对齐后的占用的大小。前面通过分析,t1所占大小应该为16字节,但是t1本质上是一个objc_objct的结构体指针,objc_objct结构体内部还有一个占8字节的isa_t,即我们通过lldb调试x/4gx输出的第一段内存,因此为8+16=24字节,t2、t3同理。
- malloc_size返回的是指针所指向的内存空间所占的大小,即系统实际分配的大小。其值为class_getInstanceSize的值按16字节对齐得到,因此为32字节。
关于class_getInstanceSize、malloc_size的详解参考《OC底层系列二》-对象中的class_getInstanceSize以及malloc_size部分。
总结
- 本文主要对内存对齐的规则进行介绍和并结合实际的demo例子对结构体和对象进行详细分析