iOS 底层探索篇 —— 内存字节对齐分析

3,607 阅读6分钟

前言

一、内存对齐规则

1.对齐系数

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

2.对齐规则

  1. 数据成员对齐规则:(Struct或者Union的数据成员)第一个数据成员放在偏移为0的位置。以后每个数据成员的位置为min(对齐系数,自身长度)的整数倍,下个位置不为本数据成员的整数倍位置的自动补齐。
  2. 数据成员为结构体:该数据成员的内最大长度的整数倍的位置开始存储。
  3. 整体对齐规则:数据成员按照1,2步骤对齐之后,其自身也要对齐,对齐原则是min(对齐系数,数据成员最大长度)的整数倍。

二、结构体内存分析

1.不用变量的内存分析

struct Struct1 {
    double a;
    int b;
    char c;
    short d;
}myStruct1;

struct Struct2 {
    int a;
    double b;
    int c;
    char d;
}myStruct2;

NSLog(@"myStruct1 - %lu",sizeof(myStruct1));
NSLog(@"myStruct2 - %lu",sizeof(myStruct2));
  • 对于一些基本的数据类型所占用的字节大小,大家应该都是非常清楚的了。
  • 通过打印输出的结果可以看到myStruct1 - 16myStruct2 - 24.

分析:

Struct1类型 位置 补齐 Struct2类型 位置 补齐
double a [0 - 7] 0 int a [0 - 3] 4
int b [8 - 11] 0 double b [8 - 15] 0
char c [12 - 12] 1 int c [16 - 19] 0
short b [14 - 15] 0 char b [20 - 20]
  • Struct1整体对齐之后:大小为16。
  • Struct2整体对齐之后:大小为24。

2.相同变量的内存分析

struct Struct1 {
    double a;
    int b;
    char c;
    short d;
}myStruct1;

struct Struct2 {
    int a;
    double b;
    char d;
    short e;
}myStruct2;

NSLog(@"myStruct1 - %lu",sizeof(myStruct1));
NSLog(@"myStruct2 - %lu",sizeof(myStruct2));
  • 通过打印输出的结果可以看到myStruct1 - 16myStruct2 - 24.

分析:

Struct1类型 位置 补齐 Struct2类型 位置 补齐
double a [0 - 7] 0 int a [0 - 3] 4
int b [8 - 11] 0 double b [8 - 15] 0
char c [12 - 12] 1 char c [16 - 16] 1
short b [14 - 15] 0 short d [18 - 19]
  • Struct1整体对齐之后:大小为16。
  • Struct2整体对齐之后:大小为24。

3.结构体作为变量的内存分析

struct Struct1 {
    double a;
    int b;
    char c;
    short d;
}myStruct1;

struct Struct2 {
    int a;
    double b;
    char d;
    struct Struct1 myStruct1;
}myStruct2;
NSLog(@"myStruct2 - %lu",sizeof(myStruct2));
  • 通过打印输出的结果可以看到myStruct2 - 24.

分析:

Struct2 类型 位置 补齐
int a [0 - 3] 4
double b [8 - 15] 0
char c [16 - 16] 7

我们按照规则来算,成员为结构体的,按照结构体的自己内部数据成员的最大长度的整数倍储存。

Struct2 类型 位置 补齐
double a [24 - 31] 0
int b [32 - 35] 0
char c [36 - 36] 1
short d [38 - 39]
  • Struct2整体对齐之后:大小为40。

三、OC类属性内存分析

1.自定义类和属性

@interface XDPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic) char ch1;
@property (nonatomic) char ch2;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
   
    XDPerson *p1 = [XDPerson alloc];
    p1.name = @"xiedong";
    p1.age = 18;
    p1.height = 180;
    p1.sex = @"男";
    p1.ch1 = 'a';
    p1.ch2 = 'b';
    
   NSLog(@"%lu - %lu",class_getInstanceSize([p1 class]),malloc_size((__bridge const void *)(p1)));
}

输出结果 40 - 48。

  • 对象申请的内存空间 <= 系统开辟的内存空间。
  • 对象申请的内存空间是以8字节对齐方式。在objc源码里面是可以得到验证的。
  • 系统开辟内存空间是以16字节对齐方式。在malloc源码里面segregated_size_to_fit()可以看到是以16字节对齐的。

2.lldb调试查看

x/6xg p1意思代表 读取p1对象6段内存地址。

(lldb) x/6xg p1
0x600000ce0000: 0x00000001029570d0 0x0000001200006261
0x600000ce0010: 0x0000000102956098 0x00000000000000b4
0x600000ce0020: 0x00000001029560b8 0x0000000000000000
(lldb) po 0x00000001029570d0 & 0x0000000ffffffff8
XDPerson
(lldb) po 0x00000012
18
(lldb) po 0x62
98
(lldb) po 0x61
97
(lldb) po 0x0000000102956098
xiedong
(lldb) po 0x00000000000000b4
180
(lldb) po 0x00000001029560b8
男

发现OC里面程序员写的属性的顺序并不是内存里面的顺序,与结构体struct还是有一定的区别。其实这里就是编译器给进行二进制重排产生的效果。

  • 第一个内存地址是isa,是objc_object这个基类带的数据成员。后面的章节中会有所介绍。

四、内存对齐原因

  1. 内存对齐是编译器处理的。
  2. CPU读取未对齐的内存时,其性能会大大的降低,此时CPU会进入到异常状态,并且通知程序不能继续进行。
  3. CPU并不是以字节为单位来存取数据的,它会把内存当成一块一块的,其块的大小可以是2、4、8、16、32字节,每次读取都是一个固定的开销,减少内存存取次数提升应用程序的性能。

我们可以想一下,假设CPU先从0地址读取4字节到寄存器,这个时候内存是对齐的,一次读取4字节。然后在从1地址读取,先读取2字节,再读取2字节,然后再合成到寄存器,这个时候CPU的性能就会相对上一次降低,对整个应用程序的性能必定会产生相应的影响。

五、内存对齐在OC中的优点

有时候我们会思考为什么系统开辟的内存大小会大于我们申请的内存大小呢?按照8字节对齐的方式,申请的内存就可能已经存在多余的了,就拿上面的例子int和两个char就会多了两字节。

  1. 按照8字节对齐方式,对象内部里面的成员内存地址是绝对安全的。
  2. 我们无法确定申请多余的字节就在对象与对象之间,有可能会出现在对象内存段的内部某个位置,这个时候就可能会出现两个对象内存段是挨着的情况,没有那么的安全。系统开辟空间采取16字节方式,保证对象的内存空间会更大,对象与对象之间更加的安全。

学习之路,砥砺前行

不足之处可以在评论区指出