iOS底层原理 :内存对齐

370 阅读7分钟

内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。 对于大部分程序员来说,“内存对齐”对我们来说都应该是“透明的”,因为我们写代码已经不是面向这一层。但是如果你想了解更加底层的秘密,“内存对齐”对你就不应该再模糊了。

下面这段代码是对内存空间的探究:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>


@interface LQProgrammer: NSObject
{
    NSString *name;
    int age;
}
@end
@implementation LQProgrammer
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        printf("Hello, World!\n");
        LQProgrammer *p1 = [[LQProgrammer alloc]init];
        LQProgrammer *p2;
        
        printf("p1:\n对象类型的内存大小--%lu\n",sizeof(p1));
        printf("对象需要的内存大小--%lu\n",class_getInstanceSize([p1 class]));
        printf("系统分配的内存大小--%lu\n",malloc_size((__bridge const void *)(p1)));
        
        printf("p2:\n对象类型的内存大小--%lu\n",sizeof(p2));
        printf("对象需要的内存大小--%lu\n",class_getInstanceSize([p2 class]));
        printf("系统分配的内存大小--%lu\n",malloc_size((__bridge const void *)(p2)));
        
        p2 = p1;
        printf("p2指向内存空间后:\n对象类型的内存大小--%lu\n",sizeof(p2));
        printf("对象需要的内存大小--%lu\n",class_getInstanceSize([p2 class]));
        printf("系统分配的内存大小--%lu\n",malloc_size((__bridge const void *)(p2)));
        
    }
    return 0;
}
Hello, World!
p1:
对象类型的内存大小--8
对象需要的内存大小--24
系统分配的内存大小--32
p2:
对象类型的内存大小--8
对象需要的内存大小--0
系统分配的内存大小--0
p2指向内存空间后:
对象类型的内存大小--8
对象需要的内存大小--24
系统分配的内存大小--32

结果分析:

  • sizeof: sizeof计算得到结果是8,很明显这只是计算到了一个指针的大小。sizeof只是一个运算符,这个在编译时期就已经确定。所以sizeof是8字节,因为它们的本质是结构体指针,当然在32位的机器上就是4个字节了。
  • class_getInstanceSize:这个方法是runtime提供的api,用于获取类的实例对象所占用的内存大小。所以 p1 的内存大小是 24 而不是 20 ,p2 只是声明了一个指针,在没有开辟内存或者指向内存空间时,大小是0,在指向内存空间后大小也是24。
  • malloc_size:系统分配的内存大小,内存是按16字节对齐的方式,即分配的大小是16的倍数 ,不足16的倍数系统会自动填充字节,所以明明只需要24字节,但是还是分配了32字节的空间。

通过汇编我们也可以看到sizeof直接赋值了一个8,class_getInstanceSize和malloc_size都是方法调用。 image.png

内存如何对齐

内存地址如何对齐,这和我们需要存入的数据有很大的关系,下面先介绍一下各种类型在内存中占用的空间。

各种类型在内存中占用大小

image.png

内存对齐原则

  • 数据成员对齐规则: 结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在 32 位机为4字节,则要从4的整数倍地址开始存储。

  • 结构体作为成员: 如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

  • 总结: 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍.不足的要补齐。

实例分析

实践1:每个非首的数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始

上代码:

#import <Foundation/Foundation.h>


typedef struct{
    char a;
    char b;
    int c;
    int d;
    char e;
}StructP;

typedef struct{
    char x;
    double y;
    int z;
}StructQ;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        StructP p1;
        p1.a = 'a';
        p1.b = 'b';
        p1.c = 512;
        p1.d = 1024;
        p1.e = 'e';
        printf("p1空间大小:%lu\n",sizeof(p1));
        printf("p1的地址:%p\n",&p1);
        StructQ q1;
        q1.x = 'x';
        q1.y = 1024.1024;
        q1.z = 'y';
        printf("q1空间大小:%lu\n",sizeof(q1));
        printf("q1的地址:%p\n",&q1);
        
    }
    return 0;
}

这里我们创建了两个结构体类型StructPStructQ,创建一个StructP类型的p1,然后给p1赋值。打印一下p1的地址,然后查看p1是如何存储的。

image.png 首先看到存储的空间是16字节,然后使用lldb命令x/16bx查看这16个字节存储的数据。很清楚的可以看到第一个位置存储的是a第二个位置存储的是b,然后空出来两个位置。

image.png 使用lldb命令x/4wx16进制查看44字节的数据。然后使用lldb命令x/4wd以十进制查看44字节的数据,可以明确的看到我们的5121024

image.png 使用lldb命令x/1bc (0x7ffeefbff4a8+4),从0x7ffeefbff4a8位置后移4个字节,查看到我们的e所在,到此我们也得到了StructP的结构图。

image.png 可以看到空了5个位置,如果将e放到c前面的话结构体StructP就只需要12个字节了,可以少使用4个字节。

image.png

接下来再来看看结构体StructQ

image.png

StructQx存在首位然后空出来7个位置,然后用8个位置存下来我们的double类型的y,然后用4个位置存下int类型的zStructQ的结构图如下图所示:

image.png

总结

1.数据成员存放的位置的确是要在整数倍开始,int4开始,double8开始。

2.结构体大小是最大成员的倍数。StructQ最大成员长度是8,大小是8的倍数24StructP最大成员长度是4,大小也是4的倍数(没优化16,优化了12)。

实践2:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储

上代码:

struct S1 {
    int x;
    double y;
    int z;
}S1;

struct S2 {
    char a;
    struct S1 s;
    int b;
    double c;
}S2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        printf("Hello, World!\n");
        printf("S1:%lu  S2:%lu\n",sizeof(S1),sizeof(S2));
        struct S2 s2;
        s2.a = '1';
        
        s2.s.x = 2;
        s2.s.y = 1024.1024;
        s2.s.z = 3;
        
        s2.b = 4;
        s2.c = 3.3;
        
        printf("s2:%p\n",&s2);
        
    }
    return 0;
}
Hello, World!
S1:24  S2:48
s2:0x7ffeefbff480
(lldb) x/6gx 0x7ffeefbff480
0x7ffeefbff480: 0x0000000100000031 0x0000000000000002
0x7ffeefbff490: 0x40900068db8bac71 0x0000000000000003
0x7ffeefbff4a0: 0x0000000000000004 0x400a666666666666
(lldb) x/f 0x7ffeefbff4a8
0x7ffeefbff4a8: 3.2999999999999998
(lldb) x/d 0x7ffeefbff4a0
0x7ffeefbff4a0: 4
(lldb) x/d 0x7ffeefbff498
0x7ffeefbff498: 3
(lldb) x/f 0x7ffeefbff490
0x7ffeefbff490: 1024.1024
(lldb) x/d 0x7ffeefbff488
0x7ffeefbff488: 2
(lldb) x/1bc 0x7ffeefbff480
0x7ffeefbff480: '1'

分析: 这里我们创建了一个S1,S2两个结构体,输出了S1、S2的大小,分别是24、48个字节,然后创建了一个S2的的结构体s2,打印了ss的地址,通过lldb查看结构体内部存储,s2.a放在了首位,摆放在0位置上,然后是我们的结构体S1,S1最长8个字节,所以从8位置开始存放,s2.s.x存在8位置占用[8-11],然首是个double,得从16位置开始,占用[16-23],s2.s.z为int型,摆在[24-27]位置上,因为结构体是个整体,所以s2.b从32开始占用[32-35],然后是s2.c从40开始摆放在[40-47]位置上。

//TODO:未完待续...