0%

iOS-底层原理03:内存对齐原理探究

前言

在开始探究内存对齐原理之前,我们先来两个问题:

  1. 内存对齐原则是什么?
  • 数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说数组、结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)
  • 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(strcut a里存有 struct b,b里有结构体char,int 等,那么b应该从8的整数倍开始存储)
  • 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的需要补齐
  1. 获取内存大小的方法有哪些?
  • sizeof
  • class_getInstanceSize
  • malloc_size

下面我们通过程序运行来看一下三种获取内存大小的方法有什么不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"objc对象类型占用的内存大小:%lu",sizeof(objc));
NSLog(@"objc对象实际占用的内存大小:%lu",class_getInstanceSize([objc class]));
NSLog(@"objc对象实际分配的内存大小:%lu",malloc_size((__bridge const void*)(objc)));
}
return 0;
}

输出结果分别为

1
2
3
objc对象类型占用的内存大小:8
objc对象实际占用的内存大小:8
objc对象实际分配的内存大小:16

由上述结果我们可得出并验证:

  • sizeof:计算类型占用的内存大小,其中可以放基本数据类型、对象、指针,对于类似于int这样的基本数据而言,sizeof获取的就是数据类型占用的内存大小,不同的数据类型所占用的内存大小是不一样的,而对于类似于NSObject定义的实例对象而言,其对象类型的本质就是一个结构体(即 struct objc_object)的指针,所以sizeof(objc)打印的是对象objc的指针大小,我们知道一个指针的内存大小是8,所以sizeof(objc) 打印是 8。(注意:这里的8字节与isa指针一点关系都没有!),对于指针而言,sizeof打印的就是8,因为一个指针的内存大小是8,
  • class_getInstanceSize:计算对象实际占用的内存大小,这个需要依据类的属性而变化,如果自定义类没有自定义属性,仅仅只是继承自NSObject,则类的实例对象实际占用的内存大小是8,可以简单理解为8字节对齐
  • malloc_size:计算对象实际分配的内存大小,这个是由系统完成的,可以从上面的打印结果看出,实际分配的和实际占用的内存大小并不相等,具体可参考并了解源码分析中的16字节对齐算法来解释这个问题

结构体内存对齐

下面我们开始进入正题:内存对齐原理的探究
众所周知,结构体和类的内存对齐方式类似,接下来,我们通过两个结构体来展开内存对齐原理的探究:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1、定义两个结构体
struct Mystruct1{
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
}Mystruct1;

struct Mystruct2{
double b; //8字节
int c; //4字节
short d; //2字节
char a; //1字节
}Mystruct2;

//计算 结构体占用的内存大小
NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));

上述两个结构体,通过控制台打印输出分别得到sizeof(Mystruct1)=24,sizeof(Mystruct2)=16,那么两个变量及变量类型都相同的结构体,仅仅是排列顺序不同,他们所占用的内存大小为什么不一样呢?这就是我们所要探究的内存字节对齐现象
通过文章开头内存对齐原则计算MyStruct1的内存大小,详解过程如下:

  • 变量a:占1个字节,从0开始,此时min(0,1),即 0 存储 a
  • 变量b:占8个字节,从1开始,此时min(1,8),1不能整除8,继续往后移动,知道min(8,8),从8开始,即 8-15 存储 b
  • 变量c:占4个字节,从16开始,此时min(16,4),16可以整除4,即 16-19 存储 c
  • 变量d:占2个字节,从20开始,此时min(20, 2),20可以整除2,即20-21 存储 d

因此MyStruct1的需要的内存大小为 15字节,而MyStruct1中最大变量的字节数为8,所以 MyStruct1 实际的内存大小必须是 8 的整数倍,21向上取整到24,主要是因为24是8的整数倍,所以 sizeof(MyStruct1) 的结果是 24

根据内存对齐规则计算MyStruct2的内存大小,详解过程如下:

  • 变量b:占8个字节,从0开始,此时min(0,8),即 0-7 存储 b
  • 变量c:占4个字节,从8开始,此时min(8,4),8可以整除4,即 8-11 存储 c
  • 变量d:占2个字节,从12开始,此时min(12, 2),12可以整除2,即12-13 存储 d
  • 变量a:占1个字节,从14开始,此时min(14,1),即 14 存储 a

因此MyStruct2的需要的内存大小为 15字节,而MyStruct1中最大变量的字节数为8,所以 MyStruct2 实际的内存大小必须是 8 的整数倍,15向上取整到16,主要是因为16是8的整数倍,所以 sizeof(MyStruct2) 的结果是 16

struct

结构体嵌套结构体

上面的两个结构体只是简单的定义数据成员,下面来一个比较复杂的,结构体中嵌套结构体的内存大小计算情况

1
2
3
4
5
6
7
8
9
10
11
12
//1、结构体嵌套结构体
struct Mystruct3{
double b; //8字节
int c; //4字节
short d; //2字节
char a; //1字节
struct Mystruct2 str;
}Mystruct3;

//2、打印 Mystruct3 的内存大小
NSLog(@"Mystruct3内存大小:%lu", sizeof(Mystruct3));
NSLog(@"Mystruct3中结构体成员内存大小:%lu", sizeof(Mystruct3.str));

分析Mystruct3的内存计算
根据内存对齐规则,来一步一步分析Mystruct3内存大小的计算过程

  • 变量b:占8个字节,从0开始,此时min(0,8),即 0-7 存储 b
  • 变量c:占4个字节,从8开始,此时min(8,4),8可以整除4,即 8-11 存储 c
  • 变量d:占2个字节,从12开始,此时min(12, 2),20可以整除2,即12-13 存储 d
  • 变量a:占1个字节,从14开始,此时min(14,1),即 14 存储 a
  • 结构体成员str:str是一个结构体,根据内存对齐原则二,结构体成员要从其内部最大成员大小的整数倍开始存储,而MyStruct2中最大的成员大小为8,所以str要从8的整数倍开始,当前是从15开始,所以不符合要求,需要往后移动到16,16是8的整数倍,符合内存对齐原则,所以 16-31 存储 str

因此MyStruct3的需要的内存大小为 32字节,而MyStruct3中最大变量为str, 其最大成员内存字节数为8,根据内存对齐原则,所以 MyStruct3 实际的内存大小必须是 8 的整数倍,32正好是8的整数倍,所以 sizeof(MyStruct3) 的结果是 32

其内存存储情况如下图所示
嵌套struct

-------------本文结束感谢您的阅读-------------