iOS底层-内存对齐
一、什么是内存对齐?
我们先看下以下例子:
struct struct0 {
int a;
char c;
}s;
NSLog(@"s : %lu", sizeof(s)); //输出8
在32位下
,int
占4byte
, char
占1byte
,那么放到结构体应该是4+1=5byte
啊,但实际得到的结果却是8
,这就是内存对齐
导致的。
元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。 从结构体存储的
首地址开始
,每个元素放置到内存中时,它都会认为内存是按照自己的大小(通常它为4或8)来划分的,因此元素放置的位置一定会在自己宽度的整数倍
上开始,这就是所谓的内存对齐
二、为什么要进行内存对齐?
1.平台限制
- 各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取,它一般会以
双字节,四字节,8字节,16字节甚至32字节
为单位来存取内存。
2.性能原因
- 例如在
32位
下的int
型变量,如果按照对齐的方式,读一次就可以读出,如果忽略对齐,int
存放在奇数位或者不在4的倍数位上,则需要读两次,再进行拼接才能得到数据。- 例如,没有内存对齐机制,一个
int
变量从地址1
开始存储,如下图:
这种情况,处理器需要:剔除不要的
,再合并
到寄存器,做了些额外的操作。而在内存对齐
的环境中,一次就可以读出数据,提高了效率。
三、内存对齐规则
1. 概念:
- 每个特定平台上的编译器都有自己的默认
对齐系数
(也叫对齐模数)。可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16
来改变这一系数,这个n
就是要制定的对齐系数
。 - 有效对齐值:给定值
#pragma pack(n)
和结构体中最长数据类型长度中较小
的那个。有效对齐值
也叫对齐单位
。 Xcode
中默认#pragma pack(8)
,也就是8字节对齐
。
2. 规则:
- (1): 结构体
第一个成员的偏移量(offset)为0
,以后每个成员相对于结构体首地址的 offset都是该成员大小与有效对齐值中较小那个的整数倍
,如有需要编译器会在成员之间加上填充字节
。 - (2): 结构体的总大小为
有效对齐值的整数倍
,如有需要编译器会在最末一个成员之后加上填充字节
。
3.举例:
结构体
环境:(Xcode 12.3)
例一:
struct Struct1 {
double a;
char b;
int c;
short d;
}s1;
分析:
- 对齐系数为:
pack(8)
,也就是8字节
,最长的数据类型是:double
,也是8字节
,则 有效对齐值:min(8,8) = 8
- 根据
规则(1)(2)
,可得出:
每个成员的大小都要和
有效对齐值
对比,取比较小的数,然后这个数的整数倍作为这个成员变量的offset起始存储位置
,如果中间不是整数倍,则需要填充字节。成员变量存储完后,如果整体大小不是有效对齐值
的整数倍,则需要在 末尾填充字节 至整体大小为有效对齐值
的整数倍为止。
分析的结果是 24字节
,我们打印来验证下:
打印的结果和我们分析的一样~
例二:
struct Struct2 {
double a;
int b;
char c;
short d;
}s2;
printf("\n Struct1 size: %lu\n", sizeof(s2)); // 输出 16
这两个结构体成员变量都是一样的,只不过位置换了下,怎么内存大小也变了?啊这...
我们继续来分析下:
分析得到的结果是16字节
,我们再来打印验证下:
原来结构体成员变量的顺序
对内存也是有影响的
例三:
struct Struct3 {
double a; // 8 [0,7]
int b; // 4 [8,11]
char c; // 1 [12]
short d; // 2 (13, [14,15]
int e; // 4 [16, 19]
struct Struct1 {
double a; // 8 (20, 21, 22, 23, [24, 31]
char b; // 1 [32]
int c; // 4 (33, 34, 35 [36, 39]
short d; // 2 [40, 41],42, 43, 44, 45, 46, 47, 48
}s1;
}s3;
分析:
- 对齐系数为:
pack(8) = 8
,Struct1
中最大为8字节
,Struct3
其他的成员最大为8字节
, 则Struct3
有效对齐值:min(pack(8),8) = 8
打印验证下:
结果是正确的~
可能存在的误区
offset
:计算成员的offset
时,有可能会将成员大小的整数倍当做其offset
,这个是错误的,offset
是由有效对齐值
和成员大小
二者的最小值的整数倍决定的整体大小
:整体大小(如果需要补充字节,就是补充后的)是最大成员的整数倍,这是不准确的。整体大小是有效对齐值
的整数倍
我们在举一个例子:
32位下
// 32 位下对齐系数 pack(4)
struct Struct4 {
int a; // 4 [0,3]
double b; // 8 [4,11]
}s4;
printf("\n\n Struct4 size: %lu\n", sizeof(s4)); // 输出 12
64位下
// 64 位下对齐系数 pack(8)
struct Struct4 {
int a; // 4 [0,3]
double b; // 8 (4, 5, 6, 7, [8, 15]
}s4;
printf("\n\n Struct4 size: %lu\n", sizeof(s4)); // 输出 16
打印如下:
- 在32位中,
Struct4
的有效对齐值
是min(pack(4), 8) = 4
,a
从首地址开始,b
的offset
是min( min(pack(4), 8), 8) = 4
,所以得出结果为12
- 在64位中,
Struct4
的有效对齐值
是min(pack(8), 8) = 8
,a
从首地址开始,b
的offset
是min( min(pack(8), 8), 8) = 8
,所以得出结果为16
总结
- 计算成员的
offset
是由有效对齐值
和成员大小
二者的最小值的整数倍决定的。 - 整体大小(如果需要补充字节,就是补充后的)是
有效对齐值
的整数倍。 - 结构体成员的
顺序不同会导致内存不同
,而对象的本质就是一个结构体,我们可以调整对象属性的位置
来达到内存优化的目的。
对象
获取对象内存大小的方式
sizeof
class_getInstanceSize
malloc_size
sizeof
sizeof
运算符,而不是一个函数sizeof
传进来的是类型,用来计算这个类型占多大内存,这个在编译器编译阶段
就会确定大小并直接转化成8 、16、24
这样的常数,而不是在运行时计算。参数可以是数组、指针、类型、对象、结构体、函数
等。- 它的功能是:获得保证能容纳实现所建立的最大对象的字节大小
class_getInstanceSize
class_getInstanceSize
是runtime
提供的api
,计算对象实际占用的内存大小
,采用8字节对齐
的方式进行运算。
malloc_size
malloc_size
用来计算对象实际分配的内存大小
,这个是由系统采用16字节对齐
的方式运算的
可以通过下面代码测试:
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end
// LGPerson 中有4个属性 NSString *name,NSString *nickName,int age,long height
int main(int argc, const char * argv[]) {
LGPerson *person = [LGPerson alloc];
person.name = @"Cc";
person.nickName = @"KC";
NSLog(@"\n%lu\n - %lu\n - %lu\n", sizeof(person), class_getInstanceSize([LGPerson class]), malloc_size((__bridge const void *)(person)));
// 输出 8, 40, 48
return 0;
}
链接:https://juejin.cn/post/6971750424198152200