iOS探索底层-结构体内存对齐

前言

在大家的工作中,或多或少都会接触到内存对齐这个概念,而内存对齐到底是什么呢,今天我就准备来探索一下这个神秘的东西。话不多说,我们先来看下对象在内存中的存储。

首先我们继续创建一个DMPerson的类,并且实例化一个对象然后对其中的各个属性进行赋值

@interface DMPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) float height;
@property (nonatomic, assign) char flag;

@end

DMPerson *p1 = [[DMPerson alloc] init
p1.name = @"mantou";
p1.nickName = @"MT";
p1.age = 16;
p1.height = 195.3;
p1.flag = 'a';
复制代码

然后我们来查看一下他在内存中的存储

image.png
其中第一列是该对象在内存中的地址,而后面的是对应的值。同样是上面的代码,我们再来加上一点打印,看看我们创建的对象在内存中,对象类型的内存大小,对象至少需要的内存大小,系统实际分配的内存大小

NSLog(@"对象类型的内存大小--%lu",sizeof(p1));
NSLog(@"对象至少需要的内存大小--%lu",class_getInstanceSize([p1 class]));
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(p1)));
复制代码

其结果如下

image.png
其结果有点出乎我的意料,对象需要的内存大小与系统实际分配的内存大小并不一样。下面我们先给出结论:

  • 在 C 语言中,sizeof() 是一个判断数据类型或者表达式长度的运算符。sizeof()的计算发生在编译时刻,所以它可以被当作常量表达式使用.而sizeof(p1)打印的是p1的类型也就是对象,本质上他是结构体指针
  • class_getInstanceSize打印出的结果是对象至少需要的内存大小,在这里我们创建的DMPerson类一共有5个属性,其中2个NSString,1个int,1个float,1个char类型。再加上类本身isa需要的8个字节,因此DMPerson类至少需要8+8+8+4+4+1 = 33然后8字节对齐,因此打印输出是40。
  • malloc_size打印的结果是系统分配的实际内存大小,前面我们计算出DMPerson类至少需要40个字节才能完整的存储下来,而在内存中对类的实际分配是16字节对齐,因此最后结果是48。

内存对齐

上面的探索中,我们有了解到不同的数据类型,在内存中占用的大小也是不一样的,我们先来偷一张图片,看看各种类型所占的字节大小(假装@Cooci)

image.png

首先我们来假设,我们有一个对象,他有1个double类型的属性,1个bool类型的属性,1个int类型的属性。那么根据表格中的对照,我们一共需要8+4+1=13个字节来存储他,如果按照顺序排列那么他将会是下图这样的情况

image.png
每次读取的数据的时候,都需要变换我们读取的大小,例如先读取8字节,读取到了double,然后再在读取8字节,发现不成功,再读取1个字节,读取到了bool,然后再往后读取1字节,又发现不成功,最后改成读取4个字节,这样效率非常的低,因此系统在存储数据的时候做了一些优化,也就是我们所谓的内存对齐。实际应该是按下图所示去存储

image.png

我们再来看一个例子:

struct LGStruct1 {
    double a;      
    char b;         
    int c;          
    short d;        
}struct1;

struct LGStruct2 {
    double a;       
    int b;          
    char c;       
    short d;  
}struct2;

NSLog(@"%lu-%lu",sizeof(struct1),sizeof(struct2));
复制代码

这两个结构体,里面包含的元素一模一样,那么他们sizeof打印出的结果是不是就一样的呢,我们来看看结果:

2021-06-09 00:20:14.623807+0800 001-内存对齐原则[9031:384474] 24-16
复制代码

结果出乎意料,两个包含内容完全相同的结构体,仅仅是因为成员变量顺序的不同,最后sizeof出来的结果也是不一样的,那么这是为什么呢?因为内存对齐,需要遵守一定的原则.

内存对齐的原则

  1. 数据成员对齐原则:结构(struct)(或者联合(union))的数据成员,第一个数据成员放在offset0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(例如int4字节,则要从4的整数倍地址开始存储)
  2. 结构体作为成员:如果一个结构体有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct bb里有char,int,double等元素,那么b应该从8的整数倍开始存储。)
  3. 收尾工作:结构体的大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐

结构体对齐(无嵌套)

就以上面的两个结构体为例,遵循内存对齐的原则,他们分别都是怎样去存储数据的呢,如下图所示

image.png
最后所占字节数为18,根据内存对齐原则的最后一条,需要补齐为其内部最大成员double a所占空间8字节的整数倍,所以等于24字节。
image.png
最后所占字节数为16,根据内存对齐原则的最后一条,16字节为其内部最大成员double a所占空间8字节的整数倍,所以等于16字节。

最后我们得出了跟上面打印结果一样的结论,说明系统确实是按照这个原则来进行数据存储的

结构体对齐(结构体嵌套)

下面我们再来看一个结构体嵌套的例子

struct LGStruct1 {
    double a;      
    char b;         
    int c;          
    short d;        
}struct1;

struct LGStruct3 {
    double a;
    int b;   
    char c;   
    short d;  
    int e;   
    struct LGStruct1 str; 
}struct3;

NSLog(@"%lu",sizeof(struct3));
复制代码

同样我们先看下系统的结果

2021-06-09 00:44:54.039441+0800 001-内存对齐原则[9380:400671] 48
复制代码

而他具体存储思路,如图所示

image.png
image.png
image.png
由于struct LGStruct1 str也是一个结构体,因此其内部也需要进行字节对齐,最后str所占的字节数是24字节。按照内存对齐原则的第二条,str需要从24的位置开始存储占用24个字节,占用字节数为48字节。

再看下面两个例子

struct DMStruct1 {
    double a;
    char b;
    int c;
    short d;
}struct1;

struct DMStruct2 {
    short a;
    char b;
}struct2;

struct DMStruct3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct DMStruct2 sss;
    struct DMStruct1 str;
}struct3;

struct DMPerson4 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct DMStruct1 str;
    struct DMStruct2 sss;
}struct4;


NSLog(@"%lu-%lu",sizeof(struct3),sizeof(struct4));
复制代码

他们的最后内存分布,入下图所示

image.png
最后struct3的结果是48字节,而struct4的结果是56字节。

2021-06-09 02:24:50.797370+0800 001-内存对齐原则[11167:486569] 48-56
复制代码

我们可以看到,结构体中的结构体,也是以整体的形式进行存储的,其内部也需要进行内存对齐。

至此,我们可以得出对于结构体,系统确实是按照内存对齐原则来进行存储数据的。那么我们知道,在iOS中,对象的本质实际上就是结构体,是不是能用结构体内存对齐的理论来验证呢,我们继续往下探索

内存优化

我们修改一下上面的代码,改变一下DMPerson中属性的位置

@interface DMPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) char flag;
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) float height;

@end
复制代码

然后继续按照上文的赋值,进行打印

image.png
发现与之前的输出结果,完全一致。然后我们逐个打印其中的数据
image.png
我们发现实际上系统在对象层面已经对内存对齐进行了优化,不管你属性是怎么样排列的,系统都会按照最优的顺序,将数据存储到内存中去。

总结

系统的数据存储都是根据内存对齐原则来进行对齐的,对于结构体,可能会因为成员顺序的问题,造成一定的内存空间浪费,而对于对象来说,在iOS的系统层面已经帮我们进行了内存优化,保证了最优的存储方式。因此,在后续的开发过程中,如果使用到了结构体类型,需要特别注意内存对齐的相关问题。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享