触摸iOS底层:内存对齐

386 阅读7分钟

一、内存对齐规则:

  • 【规则一】数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储, 反之继续检查 m+1位置 能否整除 所需的位数n, 直到可以整除, 从而就确定了当前成员的开始位置。

  • 【规则二】数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8

  • 【规则三】最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐。

二、案例和图示

上面的文本,比较枯燥,我们用图来讲解 定义两个结构体 structA 和 structB,我们就用上面的规则来猜这两个结构体在内存中如何开辟内存?内存占用多少? 任我们宰割的结构体先准备上

struct TestStructA {
    double  a; // 8 byte
    int     b; // 4 byte
    char    c; // 1 byte
    short   d; // 2 byte
} structA;


struct TestStructB {
    double  a; // 8 byte
    char    b; // 1 byte
    int     c; // 4 byte
    short   d; // 2 byte
} structB;

不清楚OC的基本数据类型在32位和64位上占用的,可以参考下面的表格 image.png

先看structA

struct TestStructA {
    double  a; // 8 byte
    int     b; // 4 byte
    char    c; // 1 byte
    short   d; // 2 byte
} structA;

1、a :根据规则1,structA的第一个成员a是double类型,偏移从0开始,a占用8个字节,range:【0,8】,如下: a的布局

2、b 的内存占用,b是int类型,占4个字节,接着a从地址 8 开始,而8%4==0,七号起始地址可以整除当前要存的b的大小,所以b从8开始,占4个字节,range:【8,12】,如下: image.png

3、c 是char,需要一个字节,继续b从地址12开始,12%1==0,所以c从12开始,占一个字节,range:【12,13】,如下: image.png

4、终于轮到了d,继续c之后从13开始,d是short需要2个字节,但是13%2!=0 , 那就继续下一个位置14开始,14%2!=0, OK,我们就从14开始存,占2个字节,range:【14,16】,如下: image.png

我们的c和d之间空了一个位置,也就是13,这个位置系统会自动补0.

根据规则三,当全部安排妥当,结构体的总大小是不是最大成员的整数倍,也就是整个结构体size % 最大成员的size == 0 可成立,如果不能整除,补0直到可以整除最大成员的size。structA很显然,刚刚好 16byte,最大成员a需要8byte,可以整除,所以,第4步就是structA的内存图示。

我们来验证一下,structA是不是占用了16个字节 image.png image.png

再看structB

struct TestStructB {
    double  a; // 8 byte
    char    b; // 1 byte
    int     c; // 4 byte
    short   d; // 2 byte
} structB;

1、a需要8个字节,从0位置开始,和structA一样: a的布局 2、b是char类型,需要一个字节,此时应该从位置8继续, 位置8 % b所需的1个字节 == 0,所以,从位置8开始,占一个位置,range:【8,9】 image.png 3、c是int,需要4个字节的位置,此时继续从9开始,显然9不能整除4,继续下一个位置10......直到12可以整除4,所以c从位置12开始,range:【12,16】 image.png 细心的朋友会发现:structA到位置16就已经结束了,structB的d还没开始计算。 4、d是short,需要2个字节,此时位置继续从16开始,16 % 2 == 0, 所以可以从位置16开始开辟,需要2个字节,所以range:【16,18】 image.png 这就结束了?????显然不是!structB总大小18 明显不能整除 最大成员a所需的8个字节,直到补0到位置24,才可以整除8。 image.png

最后,structB实际需要24个字节,我们来验证一下 image.png

structA和structB看似一样的写法,所需的内存空间却是大不相同 image.png 所以,了解内存对齐,是开发者的一项基本功。

问题来了,如果是下面的这种结构体,他的内存占用是什么样的?

struct TestStructC {
    char    *name;
    int     age;
} structC;

先查看结果他所占用的字节: image.png

16个字节!

其实很好理解,char虽然只有一个字节,但是 name是个指针变量 ,指针作为一个变量,需要 8个字节 。name指向的具体内容才是1个字节。 所以在structC里,name指针变量才是成员。

当然了,最后检查一遍:整个结构体的size是不是最大成员size的整数倍!不是,要补全

image.png

引申:对象的大小呢? 我们在开发时,会定义 LYPerson *person = [LYPerson.alloc init]; 本质上,person是一个指针,指向了这个对象在内存中的实际位置。 所以当我们sizeof(person)的时候,实际上,是获得这个指针变量的大小!指针需要8个字节!和person的属性内容无关! image.png

三、了解了结构体的内存对齐,来看一看对象属性的字节对齐

定义了这么一个类

@interface LYPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@property (nonatomic) char c3;
@property (nonatomic) char c4;
@end

初始化,先不给属性赋值

LYPerson *person = [LYPerson.alloc init];

我们来看一看person对象的内存分布 通过po person查看内存地址 然后使用 x/4gx 内存地址x/8gx 内存地址 分别查看4个数量和8个数量,每个分布需要8个字节,每一行有2个分布,也就是16字节。 image.png

每一行,如0x600000d72500: 0x00000001099a87a8 0x0000000000000000表示属性在内存中的地址,一行共16个字节。 我们打印看一下0x00000001099a87a8,只有他不为空 image.png

0x00000001099a87a8地址里的值是类名!一行总共16个字节,他就占了8个字节。

1、我们开始给属性赋值,并打上断点,先给name赋值,并查看内存变化:

LYPerson *person = [LYPerson.alloc init];
person.name      = @"一草一夜一孤城";
person.age       = 27;
person.c1        = 'a'; // ascii码 97
person.c2        = 'b'; // ascii码 98
person.c3        = 'A'; // ascii码 65
person.c4        = 'B'; // ascii码 66

image.png

0x6000029aad70出开始的16个字节区域内,多了一个子偏移地址:0x000000010d4e9038,里面存储值是属性name的值一草一叶一孤城

2、断电继续往下走,给age赋值,并查看内存变化:

image.png

有一个内存地址变成了0x1b,它里面存储了age 的内容 27

3、断点继续往下走,给c1赋值: image.png 同样是在 0x6000029aad60区域,多了一个内存地址0x61,值是c1的‘a’。 4、断点往下走,给c2赋值: image.png 它是紧挨着c1的地址0x61开始存,他是0x62,说明什么,char类型只占1个字节!

5、继续往下走,给c3赋值: image.png

6、断点继续走,给c4赋值: image.png

完成了,最终,成了这样: image.png 分别存储了name、age、c1、c2、c3、c4,但是,却不是按我们赋值的顺序存放,这是因为,苹果为我们做了字节重排优化!