IOS 底层原理之内存对齐

前言

在探究内存对齐之前,我们先了解下计算内存大小的三种方式,因为接下来在探讨内存对齐时候,我们需要用到其中的方法,首先定义一个LWPweson类(没有自定义的属性和变量)

     LWPerson * p = [LWPerson alloc];
     LWPerson * q;
        
     NSLog(@"对象类型占用内存大小--%lu",sizeof(p));
     NSLog(@"对象类型占用内存大小--%lu",sizeof(q));
     NSLog(@"对象实际内存大小-----%lu",class_getInstanceSize([p class]));
     NSLog(@"对象实际内存大小-----%lu",class_getInstanceSize([q class]));
     NSLog(@"系统为分配的内存大小--%lu",malloc_size((__bridge const void *)(p)));
     NSLog(@"系统为分配的内存大小--%lu",malloc_size((__bridge const void *)(q)));

打印结果如下

     2020-09-19 22:59:42.507118+0800 KCObjcTest[4003:298596] 对象类型占用内存大小--8
     2020-09-19 22:59:42.507678+0800 KCObjcTest[4003:298596] 对象类型占用内存大小--8
     2020-09-19 22:59:42.507780+0800 KCObjcTest[4003:298596] 对象实际内存大小-----8
     2020-09-19 22:59:42.507844+0800 KCObjcTest[4003:298596] 对象实际内存大小-----0
     2020-09-19 22:59:42.507910+0800 KCObjcTest[4003:298596] 系统分配的内存大小--16
     2020-09-19 22:59:42.507963+0800 KCObjcTest[4003:298596] 系统分配的内存大小--0

结果

  • sizeof 传进来的是类型,用来计算这个类型占多大内存,这个在编译器编译阶段就会确定,所以sizeof(p)sizeof(q)的结果都是一样的,pq都是指针类型,指针大小就是8个字节
  • class_getInstanceSize对象的实际内存大小,大小由类的属性和变量来决定,实际上并不是严格意义上的对象内存大小,因为底层进行8字节对齐算法define WORD_MASK 7UL ((x + WORD_MASK) & ~WORD_MASK ,LWPerson类中没有其他的属性和变量,但是继承了NSObjectNSObject中有一个isa指针,所以内存大小是8字节
  • malloc_size系统分配的内存大小是按16字节对齐的方式,即是按16的倍数分配 ,不足则系统会自动填充字节(具体的calloc详细流程后续会更新)

内存对齐

内存对齐原则

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

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

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

内存对齐原则描述的有点多有点复杂,下面会用实例进行详细的说明

各类型所占字节

结构体对齐

对象的本质就是结构体(对象在底层编译成结构体),结构体对齐实际上可以看做是内存对齐,只不过对象可以进行内存优化,接下来,我们用实例进行探究结构体对齐

struct LWStruct1{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
}LWStruct1;


struct LWStruct2{
    long    a; // 8
    char    d; // 1
    int     b; // 4
    short   c; // 2
 
}LWStruct2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"-----%lu------%lu",sizeof(LWStruct1),sizeof(LWStruct2));
    }
    return 0;
}
2020-09-20 15:29:13.535102+0800 KCObjcTest[1632:89696] -----16------24

从打印结果我们发现 LWStruct1LWStruct2 所包含的变量是一样的,只是位置不一样,但是内存大小不一样,为什么? 其实这就是我们一直说的内存对齐

下面我们就根据内存对齐原则来进行简单的分析和计算
LWStruct1内存大小的详细过程

  • 变量a: 占8个字节,从0开始,min(0,8),即0 ~ 7 存储a
  • 变量b: 占4个字节,从8开始,min(8,4),即8 ~ 11 存储b
  • 变量c: 占2个字节,从12开始,min(12,2),即12~ 13 存储c
  • 变量d: 占1个字节,从14开始,min(14,1),即14存储d
    因此LWStruct1的内存大小是15字节,而LWStruct1中最大的变量是a8个字节,所以LWStruct1需要实际内存必须是8的倍数15字节不是8的倍数,所以系统自动填充成16字节,最终sizeof(LWStruct1)的大小是16
    LWStruct1解析图如下

LWStruct2内存大小的详细过程

  • 变量a: 占8个字节,从0开始,min(0,8),即0 ~ 7 存储a
  • 变量d: 占1个字节,从8开始,min(8,1),即8 存储d
  • 变量b: 占4个字节,从9开始,min(9,4)9 % 4 != 0,继续往后移动直到找到可以整除4的位置 12min(12,4),即12 ~ 15 存储b
  • 变量c: 占2个字节,从16开始,min(16,2),即16 ~ 17存储c
    因此LWStruct2的内存大小是18字节,而LWStruct2中最大的变量是a8个字节,所以LWStruct2需要实际内存必须是8的倍数18字节不是8的倍数,所以系统自动填充成24字节,最终sizeof(LWStruct2)的大小是24
    LWStruct2解析图如下

结构体中嵌套结构体

继续用实例探究

struct LWStruct2{
    long    a; // 8
    char    d; // 1
    int     b; // 4
    short   c; // 2
 
}LWStruct2;

struct LWStruct3{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
    struct LWStruct2 lwStr;
}LWStruct3;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"-----%lu------%lu",sizeof(LWStruct2),sizeof(LWStruct3));
    }
    return 0;
}
2020-09-20 17:07:41.507870+0800 KCObjcTest[2467:159943] -----24------40

LWStruct3内存大小的详细过程

  • 变量a: 占8个字节,从0开始,min(0,8),即0 ~ 7 存储a
  • 变量b: 占4个字节,从8开始,min(8,4),即8 ~ 11 存储b
  • 变量c: 占2个字节,从12开始,min(12,2),即12~ 13 存储b
  • 变量d: 占1个字节,从14开始,min(14,1),即14存储d
  • 变量lwStr: 结构体变量lwStr,根据内存对齐原则结构体成员要从其内部最大元素大小的整数倍地址开始存储LWStruct2中最大的变量是8,所以从16位置开始存储,即LWStruct2存储 16-33位置,

因此LWStruct3的内存大小是34字节,而MyStruct3中最大变量为 lwStr, 其最大成员内存字节数为8,所以LWStruct13内存必须是8的倍数34字节不是8的倍数,所以系统自动填充成40字节,最终sizeof(LWStruct3)的大小是40
LWStruct3解析图如下
LWStruct3中的 lwStr任然按照LWStruct2排列,不在详细排列

内存优化

下面通过实例来看下内存优化是怎么优化的

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LWPerson * p  = [LWPerson alloc];
        p.nickName = @"hello";
        p.age = 18;
        p.height = 183;
        
    }
    return 0;
}

打印结果如下


下面我们在 LWPerson添加两个属性,继续探究

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LWPerson * p  = [LWPerson alloc];
        p.nickName = @"hello";
        p.age = 18;
        p.height = 183;
        p.a = 'a';
        p.b = 'b';
    }
    return 0;
}

打印结果如下


通过lldb断点打印可以看出 ,age的读取通过 0x00000012a的读取通过0x61(a的ASCII码是97)b的读取通过0x62(b的ASCII码是98)
我们神奇的发现 int agechar achar b,共用了一个8字节内存空间,而且对象的属性或者变量存储顺序和结构体的也不一样,这就是内存优化

总结

其实内存对齐只是制定了一套规则,目的是提高cpu的读取效率和安全的访问,通过字节对齐虽然这样浪费了大量的内存,但是同时又进行内存优化尽可能的降低了内存了浪费

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。