前语

在上篇文章中《OC底层原理-01-alloc底层探求》咱们提到过一个align1616字节对齐算法,这是否意味着系统在底层拓荒内存时便是以16字节对齐为准则进行拓荒的呢?下面咱们就正式开端探求内存对齐准则。

获取内存巨细的3种办法

咱们需要先知道iOS中获取内存巨细的3种办法,然后从这3种办法下手,开端探求。

  • sizeof()
  • class_getInstanceSize()
  • malloc_size()

下面咱们看一段代码,剖析下这3种办法获取内存巨细的异同。

    LHYStudent *baseStu = [[LHYStudent alloc]init];
    baseStu.name = @"加勒比海白菜";
    baseStu.sex = @"男";
    baseStu.age = 28;          //int类型
    baseStu.height = 199.9;    //double类型
    NSLog(@"%@\n - sizeof打印:%lu\n - class_getInstanceSize打印:%lu\n - malloc_size打印:%lu",baseStu,sizeof(baseStu),class_getInstanceSize([LHYStudent class]),malloc_size((__bridge const void *)(baseStu)));

咱们看下打印成果,会发现3种办法打印出来的内存巨细均不相同。

这是为什么呢?那就需要咱们了解这3种获取内存办法的具体意义是什么了?

OC底层原理-02-内存对齐探究

sizeof()

  • 是一个判别数据类型或许表达式长度的运算符,而不是一个函数;
  • 其效果便是回来一个目标或许类型所占的内存字节数;
  • 编译器对 sizeof() 的处理都是在编译阶段进行。

class_getInstanceSize()

该办法在alloc探求中剖析_class_createInstanceFromZone源码完成中,现已进行过简单的了解。

  • 该办法是runtime提供的一个API,所以咱们在调用该办法的时候需要先引入objc/runtime.h
  • 其实质是获取创立的目标至少所需的内存巨细,8字节对齐。

malloc_size()

该办法获取的是堆空间实践分配给目标的内存巨细,并且是16字节对齐。

了解了以上3种办法的实质,咱们还需要先了解各个数据类型在内存中占用的内存字节巨细,如下图所示:

OC底层原理-02-内存对齐探究

下面咱们就能够开端剖析为什么方才打印的成果分别为8、40、48了。

  • sizeof()

    咱们方才传入的baseStu是一个目标类型,而目标类型的实质是一个结构体指针,指针在内存中占有8字节,所以sizeof打印出来的便是8。

  • class_getInstanceSize()

    LHYStudent成员变量所需的内存空间:8+8+4+8+8(isa)=36,然后8字节对齐,也便是40。

  • malloc_size()

    堆空间实践分配给目标的内存巨细,并且依照16字节对齐,咱们能够看到实践分配的内存巨细和实践所需的内存巨细并不持平。后面有时间会再出一篇具体malloc源码剖析的文章,具体剖析一下malloc流程。

结构体内存对齐

为了方便内存对齐的具体准则,咱们先来界说两个简单的结构体,经过剖析结构体占有的内存巨细作为切入点进行探求。

    struct LHYStruct1 {
        char a;      //1字节
        double b;    //8字节
        int c;       //4字节
        short d;     //2字节
    }struct1;
    struct LHYStruct2 {
        double d;     //8字节
        int c;        //4字节
        short b;      //2字节
        char a;       //1字节
    }struct2;
    NSLog(@"%lu-%lu",sizeof(struct1),sizeof(struct2));

看下打印成果:

经过打印成果,咱们发现两个结构体的中变量的数量和类型都是相同的,仅仅顺序不同,可是所占用的内存巨细却彻底不同,而这便是因为内存对齐原理形成的。

OC底层原理-02-内存对齐探究

内存对齐规矩

  1. 结构(struct)或联合(union)的数据成员,第⼀个数据成员放在offset为0的地⽅,今后每个数据成员存储的开端方位要从该成员⼤⼩或许成员的⼦成员⼤⼩(只需该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开端(⽐如int为4字节,则要从4的整数倍地址开端存储)
  2. 如果⼀个结构⾥包含结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开端存储(struct a⾥存有struct b,b⾥有char、int、double等元素,那b应该从8的整数倍开端存储)。
  3. 结构体的内存的总⼤⼩,有必要是其内部成员最大内存的整数倍,不⾜的需要补⻬。

下面咱们把上面的比如依照内存对齐规矩进行剖析说明。

结构体struct1内存巨细核算

变量a为char类型,占用1个字节,从0开端排放即【0】
变量b为double类型,占用8个字节,依照内存对齐准则,需要从8的倍数开端放,显然1不是8的倍数,直到找到8开端排放即【8-15】,中心的【1-7】均是补齐。
变量c为int类型,占用4个字节,从16开端排放,16刚好是4的倍数,即【16-19】
变量d为short类型,占用2个字节,从20开端排放,即【20-21】
struct1中最大的变量b所需字节数是8字节,struct1实践内存有必要是8的倍数,22向上取整为24,所以sizeof(struct1)的成果是24
stuct1内存散布概念图如下所示:

OC底层原理-02-内存对齐探究

结构体struct2内存巨细核算

变量d为double类型,占用8个字节,从0开端排放即【0-7】
变量c为int类型,占用4个字节,从8开端排放,8刚好是4的倍数,即【8-11】
变量b为short类型,占用2个字节,从12开端排放,12刚好也是2的倍数,即【12-13】
变量a为char类型,占用1个字节,从14开端排放即【14】
struct1中最大的变量b所需字节数是8字节,struct1实践内存有必要是8的倍数,15向上取整为16,所以sizeof(struct2)的成果是16
stuct2内存散布概念图如下所示:

剖析完这个,下面咱们难度升级,探求下结构体嵌套结构体,内存空间又是如何核算内存巨细的?

OC底层原理-02-内存对齐探究

结构体嵌套结构体

话不多说,先来界说一个结构体嵌套结构体的结构体,如下所示:

    struct LHYStruct2 {
        double d;
        int c;
        short b;
        char a;
    }struct2;
    struct LHYStruct3 {
        double d;
        int c;
        short b;
        char a;
        struct LHYStruct2 stru;
    }struct3;
    NSLog(@"struct3内存巨细:%lu\n struct3中结构体stru成员内存巨细:%lu",sizeof(struct3),sizeof(struct3.stru));

下面咱们看下输出成果:

咱们依据内存对齐规矩,来剖析下结构体嵌套结构体的内存巨细的核算过程
变量d为double类型,占用8个字节,从0开端排放即【0-7】
变量c为int类型,占用4个字节,从8开端排放,8刚好是4的倍数,即【8-11】
变量b为short类型,占用2个字节,从12开端排放,12刚好也是2的倍数,即【12-13】
变量a为char类型,占用1个字节,从14开端排放即【14】
结构体stru为结构体类型,依据内存对齐规矩第2条,stru中变量占有内存最大的变量b所需的字节是8,所以有必要从8的整数倍开端存放stru,显然15不是8的倍数,接着往后找,发现16是8的整数倍,即【16-31】

OC底层原理-02-内存对齐探究
这儿我最开端有一个疑问,结构体内存需要是内部成员中最大变量占的字节数的整数倍,struct3中最大的成员不是stru吗? 它所占的字节数是16,而32又正好是16的倍数,彻底符合内存对齐规矩啊。所以我就在struct2中再添加一个double类型的变量来验证这个观点。

    struct LHYStruct2 {
        double d;
        int c;
        short b;
        char a;
        double e;
    }struct2;
    struct LHYStruct3 {
        double d;
        int c;
        short b;
        char a;
        struct LHYStruct2 stru;
    }struct3;
     NSLog(@"struct3内存巨细:%lu\n struct3中结构体stru成员内存巨细:%lu",sizeof(struct3),sizeof(struct3.stru));

看下打印成果:

从这个打印成果来看,咱们得出一个定论,当结构体嵌套了结构体时,作为数据成员的结构体的最大成员内存巨细作为外部结构体的最大成员的内存巨细,而不是以作为数据成员的结构体巨细作为外部结构体的最大成员内存巨细。

OC底层原理-02-内存对齐探究
因此结构体嵌套的第一个示例中struct3中最大变量为stru, 其最大成员内存字节数为8,依据内存对齐准则,所以struct3实践的内存巨细有必要是8的整数倍,32正好是8的整数倍,所以sizeof(struct3)的成果是32
struct3的内存散布概念图如下所示:

OC底层原理-02-内存对齐探究

内存优化

不知道之前有没有发现,当一个类有多个特点的时候,并不是完彻底全依照字节对齐进行内存排布的,下面咱们写个demo验证一下。

    LHYStudent *baseStu = [[LHYStudent alloc]init];
    baseStu.name = @"加勒比海白菜";
    baseStu.sex = @"男";
    baseStu.age = 28;
    baseStu.height = 199.9;
    baseStu.a = 'a';
    baseStu.b = 'b';
    baseStu.hobby = @"coding";
    NSLog(@"-------%@",baseStu);

经过LLDB调试,依据baseStu的地址,打印特点的值。

咱们发现系统将a/b/age字段进行了重排存储在同一个内存块中。这应该便是苹果系统内部进行的处理优化,既避免了内存的糟蹋,又能进步读取效率和安全拜访。

OC底层原理-02-内存对齐探究

16字节对齐算法

现在咱们知道的字节对齐算法有两种。

  1. align16算法
    咱们上篇文章讲到过align16算法,这儿就不再多加赘述。
static inline size_t align16(size_t x) {
    //16字节对齐算法 &为与操作 ~为取反操作
    return (x + size_t(15)) & ~size_t(15);
}
  1. segregated_size_to_fit算法
#define SHIFT_NANO_QUANTUM		4
//将1 左移4位 0000 0001 ->  0001 0000 也便是16
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	// 16
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
	size_t k, slot_bytes;
	if (0 == size) {
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
        //中心算法
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
	slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
	*pKey = k - 1;													// Zero-based!
	return slot_bytes;
}

这两行代码是算法的中心,咱们来翻译一下。
k = (size + NANO_REGIME_QUANTA_SIZE – 1) >> SHIFT_NANO_QUANTUM;
k= 【(size + 16 - 1) 右移4位】
slot_bytes = k << SHIFT_NANO_QUANTUM;
slot_bytes = k左移4位
举个比如验证一下,假定 size=20
20+16-1=35 -> 0010 0011
右移4位 —–> 0000 0010
再左移4位 —> 0010 0000 = 32

总结

  • 目标内部来说,也便是特点和特点之间,其真实的对齐办法是8字节对齐,现在所知的最大类型占用的内存也就8字节。
  • 目标外部来说,也便是目标和目标之间,采用的是16字节对齐办法,进步内存读取容错率。
  • 系统内部会对部分类型的数据进行重排,进行内存优化。