OC底层学习-内存对齐

399 阅读9分钟

我们知道了alloc底层是开辟内存空间并关联我们的类,那么一个对象开辟的内存大小与什么有关系呢?

影响对象内存的因素是什么?

首先我们来看一个例子 image.png image.png 我们定义一个LhkhPerson,里面没有任何成员变量,属性和方法,这个时候我们打印它的内存大小的时候是8(alloc底层探索的时候讲了为什么是8) image.png 而当添加了一个NSString属性过后,大小变成了16 image.png image.png 通过上面的类似的例子我们最终可以发现,对象内存大小取决于它的属性成员变量,与其里面的方法没有关系,总结起来就是影响对象内存的因素是成员变量

在上面我们探索出来了对象内存与成员变量有关,那当我们再次加一个int型的属性时,我们打印内存大小时

image.png 又增加了8(我们知道,int型占4个字节),那为什么还是增加8字节呢?

结构体内存对齐

各个类型占用的字节大小表

image.png

结构体内存对齐三大原则

  • 数据成员对齐原则:结构体(struct)(或者联合体(union))的数据成员,第一个数据放在offset为0的地方,以后每个数据成员的存放位置要从该成员或者成员的子成员(只要该成员有子成员,例如数组,结构体等)的大小的整数倍开始;

  • 结构体作为成员:如果一个结构体中有某些结构体成员时,那么结构体成员里面的元素要从其内部元素占用最大字节数的整数倍地址开始存储;(例如struct a里面有struct b,而b里面有char,int,double三个类型的的元素,那么b就应该从8的整数倍数开始存储)

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

我们接下来结合这三个原则来看一个例子:

struct LhkhStruct1 {
    double a;       // 8 [0 7] double占用8个字节 从0开始存储,也就是到7的位置,也就是
                    元素‘a’存放在0-7
    char b;         // 1 [8] char占用1个字节,而上一个元素存到了7,那么接下来就是从8
                    开始,而8刚好又是1的倍数,所以第二个元素‘b’就是存放到8
    int c;          // 4 (9 10 11 [12 13 14 15] int占用是个4字节,上一个元素‘b’存到8,那么‘c’应该从9开始,根据‘数据成员对齐原则’9不是4的整数倍,那么
                    就会往后排,1011均是如此,124的整数倍,所以元素‘c’就应该从12开
                    始到15结束
    short d;        // 2 [16 17] 24 short占用2个字节,上一个元素‘c’存到了15的位置,
                    那么‘d’应该从16开始,而且16刚好是2的整数倍,所以‘d’存放到16-17
}struct1;           //24 文字说明解释一下原则1

struct LhkhStruct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11]
    char c;         // 1    [12]
    short d;        // 2    (13 [14 15] 
}struct2;           //16 根据上面的struct1的文字说明脑补啊

//内部包含结构体
struct LhkhStruct3 {
    double a;       // 8 [0-7]
    int b;          // 4 [8 9 10 11]
    char c;         // 1 [12]
    short d;        // 2 (13 [14 15]
    int e;          // 4 [16 17 18 19] 
    struct LhkhStruct1 str1; //根据上面的对齐原则1,str1需要17,而根据原则2结构体中最大成员8,所以应该是8的整数倍数,那也就是2420 21 22 23 [24...40] 41
}struct3;           //48 

NSLog(@"LhkhStruct1-->%ld",**sizeof**(struct1));
NSLog(@"LhkhStruct2-->%ld",**sizeof**(struct2));
NSLog(@"LhkhStruct3-->%ld",**sizeof**(struct3));

先根据3大原则我们来试着解释一下看看:

LhkhStruct1内存大小为24:根据里面的元素我们算出来需要占用0-17,也就是大小为18,而结构体里面最大的元素double8字节,所以需要取8的整数倍,而18不是8的整数倍,所以就取24

LhkhStruct2内存大小为16:根据里面的元素我们算出来需要占用0-15,也就是大小为16,而结构体里面最大的元素double8字节,所以需要取8的整数倍,而16刚好为8的整数倍,所以就取16

这边我们可以注意到,其实LhkhStruct1和LhkhStruct2里面的所包含的变量是一摸摸一样样的,也就
换了一下顺序,结果大小就不一致,这就很好的解释了系统对结构体内存对齐的优化

LhkhStruct3内存大小为48:根据里面的元素我们算出来除了str1时需要占用0-19,而我们知道str1里面的最大成员为8,根据原则2,那str1应该从24的位置开始存,从24开始后面17位,也就是0-41,根据原则3,最大成员为double的8和str1的8,所以需要取8的整数倍,所以就取48;其实这边我们也可以这么计算,我们知道除了str1时,占用了0-19,那根据原则3则需要24,而我们通过计算得到str1也需要24,那么直接24+24=48即可。

那我们再来打印验证一下我们计算的值:

image.png

我们回过头来再看一下这个例子: 这个是我们LhkhPerson image.png 那我们再来打印一下内存大小:

image.png 我们发现使用sizeof打印的结果是8class_getInstanceSize打印的结果是40malloc_size打印的结果是48,那这些是怎么来的呢?

在了解了结构体内存对齐原则过后我们再来解释一下这几个打印:

  • sizeof(p)打印的结果是8:这个是因为我们的对象p的实质是一个结构体指针,结构体指针的内存大小为8

  • class_getInstanceSize(p.class)打印的结果是40:首先我们需要理解class_getInstanceSize是计算对象里面的成员变量的内存大小(可以理解为类或者对象至少需要的内存空间),(NSString类型的name大小为8,NSString类型的nickname大小为8,int型age大小为4,double型height大小为8,char型的c大小为1,所以8+8+4+8+1=29)大小为29,那么为什么是40呢? 两个注意点:

    1. 注意父类属性的继承和isa,所以这边还应加上一个isa的8字节大小;
    2. 注意成员变量8字节对齐原则。  
    

所以这边29+8=37,然后按照8字节对齐,所以取8的倍数也就是40

  • malloc_size((__bridge const void *)p)打印结果为48:首先我们需要知道malloc_size方法是计算对象实际系统开辟的内存空间大小; 需要注意

    1.计算的结果应该是在class_getInstanceSize计算出的内存大小的基础上计算;
    2.16字节对齐原则
    

所以class_getInstanceSize计算得到内存为40,根据16字节对齐原则,这边malloc_size计算出来的结果就是48.

我们知道sizeof是一个操作符,不是一个函数,单纯用来计算数据类型的内存大小的;

class_getInstanceSize我们应该不陌生,在探究alloc底层的时候我们见过: 通过在objc源码中搜索,最终可以锁定到这个算法(x + WORD_MASK) & ~WORD_MASK,也就是8字节对齐算法;

image.png image.png image.png

malloc_size对于这个方法我们还是有点陌生的(反正我是没有用过,请忽略我是小菜鸟),结合class_getInstanceSize获取变量实际占用内存大小(8字节对齐)来看,这个就是实际分配的内存大小,且16字节对齐,那malloc内部到底怎么实现的呢?

malloc探究

我们探究一个方法,首先基本上都是command点击进去查看源代码,但是我们发现没有malloc_size的实现源码,苦涩。。。

image.png 不慌,我们完全可以像探索alloc底层一样,我们都知道malloc_size这个方法了,那直接下一个符号短点看汇编不就行了,,,汇编(yyds)😊

其实这边我们通过看到上面的malloc.h文件的路径发现已经是在usr/include/malloc文件下面了,我们应该可以猜想到源码应该在系统的malloc库中,知道这个那我们直接开车苹果开源库opensource/这个是快车,我们这边使用的是libmalloc 317.40.8版本源码为例:

那我们本着一个疑问进行探讨,在前面我们通过上面的例子获取到实际变量的内存大小为40,那为什么系统最终分配的内存为48呢?(虽然我们已经知道了是由于堆里面对象以16字节对齐)那还是得知其所以然啊,开搞------>

malloc源代码编译

我们探索alloc的时候知道calloc是系统开辟内存的方法,那我们探索malloc_size就直接从calloc下手;

通过调用calloc方法开辟40字节的内存,然后探索最终系统给开辟了多少内存:

image.png command点击calloc进入到方法实现,我们发现其内部调用了_malloc_zone_calloc

image.png command点击_malloc_zone_calloc进入到方法实现

image.png 我们进入到calloc方法中,又发现只是一个方法声明,并没有实现。。。 image.png 这个时候我们通过搜索calloc时发现

image.png 那我们就po一下zone->calloc

image.png 我们发现了这时zone->calloc存储的是这个方法default_zone_calloc,那我们通过搜索default_zone_calloc

image.png image.png 进而又发现了nano_calloc,那我们通过搜索nano_calloc

image.png 继续进入到_nano_malloc_check_clear,通过分析发现与内存相关的也就是size_t,看到后面的方法segregated_size_to_fit image.png 继续跟进,很熟悉的味道啊(探索alloc时align16)--->16字节对齐 image.png

经过上面的探究我们最终应该可以得到

  • 堆对象内存为16字节对齐
  • 对象的成员变量内部是以8字节对齐
  • 对象与对象之间以16字节对齐