iOS-底层原理:内存对齐

上篇文章中iOS-底层原理:alloc & init & new 源码分析通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,并且会通过size = cls->instanceSize(extraBytes)这一步来计算需要开辟的内存大小。那么它在计算的过程中是怎么处理的呢,或者说要遵循什么样的原则?下面我们来接着探索。

一切的一切我们还是从最初的代码开始

alloc一个无属性Person

此时我们只是创建了Person 类,其中什么信息都没有。当我们读取内存段,打印出的前8位是 Person,此时是我们的isa指针。

Person 增加 NSString 属性

// Person 类中的属性
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;

@end
  • 首先是无赋值情况
    无赋值
  • 赋值一个属性情况
    赋值一个属性
  • 赋值两个属性情况
    赋值两个属性

通过打印我们得知一个NSString类型的属性是占8位字节大小,
光是这点我们看不出什么,接着继续。。

Person 增加 int 属性

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;

@end
  • 先看下 无赋值情况
    int不赋值

这时候我们发现和之前只有NSString的时候不一样了,第二个段内存段会打印出 0x0000000000000000 ,接着继续。。

  • age赋值
    age赋值

这时候我们发现本来第二块的位置应该是name,现在被age给占了?怎么不知道先来后到?为了想知道为什么接着往下走。。

再次在Preson属性中增加个int类型的属性,并赋值

@interface Person : NSObject

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

@end
两个int赋值

从打印上看此时两个int类型ageheight共用一个8字节的内存。

接着往下看。。

Person 增加 char 属性

@interface Person : NSObject

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

@end

看下打印结果

char属性赋值

这时候我们发现一个 int 类型的和两个 char 类型的属性共用了 8 字节的内存空间。

看到这里我们脑子里有个大大的,为什么会是这种情况,那么今天的重点来了。。

内存对齐

计算机内存都是以字节为单位划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为48的倍数,这就是所谓的内存对齐

内存对齐的原因

我们都知道内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以2字节,4字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度

  • CPU的数据总线宽度决定了CPU对数据的吞吐量
  • 64CPU一次处理64 bit也就是8个字节的数据,32同样,每次处理4个字节的数据

内存对齐规则

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n)n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。在ios中,Xcode默认为#pragma pack(8),即8字节对齐。

内存对齐规则可理解为以下三点:

  • 数据成员对⻬规则:数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除n (即m % n == 0), nm 位置开始存储, 反之继续检查m+1能否整除n, 直到可以整除, 从而就确定了当前成员的开始位置。
  • 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
    其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b
    里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  • 收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员变量的整数倍.不足的要补齐.

下表是各种数据类型在ios中的占用内存大小,根据对应类型来计算结构体中内存大小,


数据类型对应的字节数表格

上面一大堆都是理论,理解着比较困难,难么下面我们就通过实践来分析。

结构体内存对齐

定义两个结构体

//1、定义两个结构体
struct Struct1{
    char a;     //1字节
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
}Struct1;

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

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

以下是输出结果:

24 - 16

两个结构体乍一看,没什么区别,其中定义的变量 和 变量类型都是一致的,唯一的区别只是在于定义变量的顺序不一致,那为什么他们做占用的内存大小不相等呢?其实这就是iOS中的内存字节对齐现象

结构体对应的存储情况

结构体Struct1内存大小计算

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

    • 变量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

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

结构体Struct2内存大小计算

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

    • 变量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

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

结构体嵌套结构体

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

//1、结构体嵌套结构体
struct Struct3{
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
    char a;     //1字节
    struct Struct2 str; 
}Struct3;

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

打印 的结果如下:

Struct3内存大小:32
Struct3中结构体成员内存大小:16
  • 分析Struct3的内存计算
    根据内存对齐规则,来一步一步分析Struct3内存大小的计算过程

    • 变量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是一个结构体,根据内存对齐原则结构体成员要从其内部最大成员大小的整数倍开始存储,而Struct2中最大的成员大小为8,所以str要从8的整数倍开始,当前是从15开始,所以不符合要求,需要往后移动到16168的整数倍,符合内存对齐原则,所以16-31 存储 str

因此Struct3的需要的内存大小为32字节,而Struct2中最大变量为str, 其内存字节数为16,所以 Struct2 实际的内存大小必须是 16 的整数倍,32正好是16的整数倍,所以 sizeof(Struct3) 的结果是 32

内存优化(属性重排)

Struct1 通过内存字节对齐原则,增加了9个字节,而Struct2通过内存字节对齐原则,通过4+2+1的组合,只需要补齐一个字节即可满足字节对齐规则,这里得出一个结论结构体内存大小与结构体成员内存大小的顺序有关

  • 如果是结构体中数据成员是根据内存从小到大的顺序定义的,根据内存对齐规则来计算结构体内存大小,需要增加有较大的内存padding即内存占位符,才能满足内存对齐规则,比较浪费内存

  • 如果是结构体中数据成员是根据内存从大到小的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足内存对齐规则,这种方式就是苹果中采用的,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的

到此,我们在回过头来看我们一开始演示的Person类,也能验证苹果中的属性重排,即内存优化。

Person
  • 苹果中针对age、s、d属性的内存进行了重排,因为age类型占4个字节,s和d类型char分别占1个字节,通过4+1+1的方式,按照8字节对齐,不足补齐的方式存储在同一块内存中。

总结

苹果中的内存对齐思想:

  • 大部分的内存都是通过固定的内存块进行读取,
  • 尽管我们在内存中采用了内存对齐的方式,但并不是所有的内存都可以进行浪费的,苹果会自动对属性进行重排,以此来优化内存

多少字节对齐:
我们上篇文章中提及了16字节对齐,我们在这里提到了8字节对齐那我们到底采用哪种字节对齐呢?

  • 对于一个对象来说,其真正的对齐方式 是 8字节对齐,8字节对齐已经足够满足对象的需求了
  • apple系统为了防止一切的容错,采用的是16字节对齐的内存,主要是因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

你可能感兴趣的:(iOS-底层原理:内存对齐)