什么是内存对齐
内存对齐是一种在计算机内存中排列数据(表现为变量的地址)、访问数据(表现为CPU读取数据)的一种方式。
它包含了两种相互独立又相互关联的部分:基本数据对齐和结构体数据对齐
为什么要内存对齐
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
iOS 结构体内存对齐
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令 #pragma pack(n),n=1, 2, 4, 8,16 来改变这一系数,其中的 n 就是你要指定的“对齐系数”。iOS 默认 8
对齐原则
-
数据成员对齐规则:结构 (struct) 或联合(union)的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如 int 为 4 字节,则要从 4 的整数倍地址开始存储)
-
结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素内存大小的整数倍地址开始存储 (struct a 里存有 struct b,b 里有 char , int , double 等元素,那 b 应该从 8 的整数倍开始存储 )
-
收尾工作:结构体的总大小,也就是 sizeof 的结果必须是其内部最大成员的整数倍,不足的要补齐
我们定义两个结构体,分别计算他们的内存大小
struct MyStruct1 {
double a;//8 (0, 1, 2, 3, 4, 5, 6, 7)
char b;// 1 (8)
int c; // 4 /*9, 10, 11 不是4的倍数,所以废弃*/(12, 13, 14, 15)
short d;// 2 (16, 17)
} struct1;
struct MyStruct2 {
int c;// 4 (0, 1, 2, 3)
char b;// 1 (4)
short d;// 2 (6, 7)
double a;// 8 (8, 9, 10, 11, 12, 13, 14, 15)
} struct2;
NSLog(@"%lu, %lu", sizeof(struct1), sizeof(struct2));
输出结果如下
两个结构体的成员个数一样,类型一样,唯一的区别是成员的顺序不一样,它们占用的内存大小就不相等,这就是iOS 中结构体内存对齐。
各种数据类型在 iOS 中的占用内存大小如下图
MyStruct1 的内存存储示意图如下
MyStruct2 的内存存储示意图如下
下面我们再来看下结构体嵌套结构体的内存存储情况,借助上述的两个结构体变量,如下所示
struct MyStruct3 {
struct MyStruct1 one;//24 /*结构体中最大元素为 8(double),从 8 的倍数位置存储(offset 为0)*/(0-23)
struct MyStruct2 two;//16 /*结构体中最大元素为 8(double),24 是 8 的倍数,开始存储*/(24-39)
int a;// 4 (40, 41, 42, 43)
double b;// 8 /*44, 45, 46, 47 不是8的倍数,废弃*/ (48, 49, 50, 51, 52, 53, 54, 55)
} struct3;
NSLog(@"%lu", sizeof(struct3));
最终的打印结果为 56
- MyStruct1 内存字节数为 24,MyStruct2 最大变量 double 为 8 字节,24 是 8 的倍数,可以从 MyStruct1 结束的位置开始存储, 内存字节数为16,变量 a 为 4 字节,40 正好是 4 的倍数,可以从 MyStruct2 结束的位置开始存储,变量 b 为 8 字节,44 不是 8 的倍数,需要从 48 的位置开始存储,即最后结果为 56
MyStruct3 的内存存储示意图如下
以上可以看出,结构体的内存对齐是按照成员类型上下读取的,这就是为什么两个结构体的成员一样而顺序不一样,他们的内存大小就会不一样的原因
iOS 对象的内存对齐
首先,创建一个工程,创建 Person 类,为 Person 类添加一些属性,如下
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickname;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) char char1;
@property (nonatomic, assign) char char2;
@end
@implementation Person
@end
为属性赋值,设置断点并运行起来,如下
Person *p = [Person alloc];
p.name = @"tlab"; //8
p.nickname = @"lc"; //8
p.hobby = @"学习"; //8
p.age = 18; //4
p.char1 = 'a';//1
p.char2 = 'b';//1
NSLog(@"%@", p);
根据打印的 Person 对象实例,查看对象的内存情况可以在lldb中用 x
、p
和 po
这些指令来查看
是不是有点懵逼?1. 为什么第一个打印的是 Person, 2. age、char1、char2 哪去了?别急,我们先看下内存情况,调用以下方法
// sizeof(p) 为 p 指针占用的内存大小,class_getInstanceSize([p class]) 对象实际使用的内存大小,malloc_size((__bridge const void *)(p)) 为系统申请的内存大小
NSLog(@"%lu---%lu---%lu", sizeof(p), class_getInstanceSize([p class]), malloc_size((__bridge const void *)(p)));
打印结果为 8---40---48
是不是跟你预想的不一样,这里需要解释一下
-指针占用 8 字节,这个没有什么好说的
- 对象实际占用的内存大小为什么是 40 而不是 32?因为对象有个默认的属性:isa,它占用 8 字节,因为对象需要 8 字节对齐,不足 8 字节的补上去,所以实际用到的内存大小为 40
- 系统开辟内存需要 16 字节对齐,所以申请开辟的内存大小为 48 字节
现在再去看上面的问题,对象默认的第一个属性是 isa,isa 指向所属的类,所以打印的是 Person
为什么 age、char1、char2 没有打印出来呢?第二个打印的不是 tlab 而是乱码呢?原因是苹果中针对 age、char1、char2 属性的内存进行了重排(内存优化),我们换个方式打印
将 0x0000001200006261 拆分打印就得到了 age、char1、char2,其中 97 和 98 分别是小写字母 a 和 b 的 ASCII 编码。为什么会这样呢?因为 Person 中的 age 是 int 类型占 4 字节,char1 和 char2 分别占 1 个字节,通过 4+1+1 的方式,按照 8 字节对齐,不足补齐的方式存储在同一块内存中。
Person 的内存分布情况如下:
网友评论