002- iOS底层源码探究之结构体、内存对齐

在上一篇文章中 iOS底层源码探究之alloc、init ​​​​​​​中我们已经探究了OC 对象的初始化过程,但是一个对象的大小又跟哪些因素有关呢?或者说一个对象需要开辟的内存空间由哪些因素决定?这里我们就来探究一下。

 在这之前,我们先来看一下获取内存大小的三种方式:

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

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"obj对象类型占用内存的大小:%lu",sizeof(obj));
        NSLog(@"obj对象实际占用的内存的大小:%lu",class_getInstanceSize([obj class]));
        NSLog(@"obj对象实际分配型内存的大小:%lu",malloc_size((__bridge const void*)(obj)));
    }
    return 0;
}

2021-08-06 17:31:07.752213+0800 TEXT[32445:473578] obj对象类型占用内存的大小:8
2021-08-06 17:31:07.753828+0800 TEXT[32445:473578] obj对象实际占用的内存的大小:8
2021-08-06 17:31:11.691566+0800 TEXT[32445:473578] obj对象实际分配型内存的大小:16

小结:

sizeof计算类型占用的内存大小,其中可以放 基本数据类型、对象、指针。对于obj实例对象而言,其本质就是一个结构体(即 struct objc_object)的指针,所以sizeof(obj)打印的是对象obj的指针大小,我们知道一个指针的内存大小是8,所以打印是 8

class_getInstanceSize: 计算对象实际占用的内存大小,这个需要依据类的属性,成员变量而变化,如果没有自定义属性和成员变量,仅仅只是继承自NSObject,则类的实例对象实际占用的内存大小是8,这是因为NSObject自带一个成员变量isa,是一个结构体指针,占8个字节。

 malloc_size: 计算对象实际分配的内存大小,这个是由系统完成的,从上面的打印结果看出,实际分配的和实际占用的内存大小并不相等,这是因为系统分配内存按16字节对齐,这个问题后面会具体说明。


我们都知道OC经过编译之后会变成C代码,对象会变成C语言中的结构体。接下来我们来研究结构体内存对齐

 话不多说,上代码

//定义两个结构体
struct SZstruct1{
    char a;     //1字节
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
}SZstruct1;

struct SZstruct2{
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
    char a;     //1字节
}SZstruct2;

//结构体嵌套结构体
struct SZstruct3{
    char a;     //1字节
    short d;    //2字节
    int c;      //4字节
    double b;   //8字节
    struct SZstruct2 str2; 
}SZstruct3;

//计算 结构体占用的内存大小
NSLog(@"%lu-%lu-%lu",sizeof(SZstruct1),sizeof(SZstruct2),sizeof(SZstruct3));

2021-08-06 17:52:16.593596+0800 TEXT[33297:490443] 24-16-32

问题:两个结构体唯一的区别只是在于定义变量的顺序不一致,那为什么他们做占用的内存大小不相等呢?其实这就是iOS中的内存字节对齐现象

分析

内存对齐规则:每个平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”,在ios中,Xcode默认为#pragma pack(8),即8字节对齐。

内存对齐原则:

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

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

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

验证对齐原则 :

首先看看不同数据类型占内存大小图

 

SZstruct1内存大小计算详细过程

1、char a: 从第0位开始,占1个字节,根据上面对齐原则一的公式min(0,1),0 % 1 = 0,可以整除,即a存储在第0位;

2、double b:从第1位开始,占8个字节,此时min(1,8),1 % 8 = 1,不能整除,所以位数向后移动直到第8min(8,8),8 % 8 = 0,可以整除,所以8~15存储b;

3、int c:从第16位开始,占4个字节,此时min(16,4),16 % 4 = 0,可以整除,所以16~19存储C;

4、short d:20位开始,占2个字节,此时min(20,2),20 % 2 = 0.可以整除,所以20~21存储d

5、根据内存对齐原则三:最后结构体的内存大小必须是结构体中最大成员即double b(8)内存大小整数倍,不足的需要补齐,所以sizeof(SZstruct1)的结果是24

​​​​​​​

SZstruct2内存大小计算详细过程

1、double b: 从第0位开始,占8个字节,根据上面对齐原则一的公式min(0,8),0 % 8 = 0,可以整除,即0~7存储b;

2、int c从第8位开始,占4个字节,此时min(8,4),8 % 4 = 0,可以整除,所以8~11存储C;

3、short d从第12位开始,占2个字节,此时min(12,2),12 % 2 = 0,可以整除,所以12~13存储d;

4、char a14位开始,占1个字节,此时min(14,1),14 % 1 = 0.可以整除,所以第14位存储a

5、根据内存对齐原则三:最后结构体的内存大小必须是结构体中最大成员即double b(8)内存大小整数倍,不足的需要补齐,所以sizeof(SZstruct2)的结果是16

SZstruct3内存大小计算详细过程

1、char a: 从第0位开始,占1个字节,根据上面对齐原则一的公式min(0,1),0 % 1 = 0,可以整除,即第0位存储a;

2、short d从第1位开始,占2个字节,此时min(1,2),1 % 2 = 1,不能整除,所以位数向后移动直到第2min(2,2),2 % 2 = 0,可以整除,所以2~3存储d;

3、int c从第4位开始,占4个字节,此时min(4,4),4 % 4 = 0,可以整除,所以4~7存储c;

4、double b8位开始,占8个字节,此时min(8,8),8 % 8 = 0.可以整除,所以8~15存储b

5、SZstruct2 str2:从第16位开始,根据内存对齐原则二:str2自身长度为8,占8个字节,此时min(16,8),16 % 8 = 0.可以整除,所以16~31存储str2

6、根据内存对齐原则三:最后结构体的内存大小必须是结构体中最大成员即str2(8)内存大小整数倍,不足的需要补齐,所以sizeof(SZstruct2)的结果是32

如果上面的文字描述的不够清楚,可以看下面的图解:

总结:结构体内存大小与结构体成员内存大小的顺序有关 。

那么字节对齐到底采用多少字节对齐? 

class_getInstanceSize是采用8字节对齐,参照的对象的属性内存大小

malloc_size:采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍

为什么分配内存采用16字节对齐呢?

(1).通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数降低cpu的开销,这就是以空间换时间。

(2).16字节对齐,是由于在一个对象中,第一个属性isa8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱

小结:这里也就解释了上一篇文章iOS底层源码探究之alloc、init中(cls->instanceSize) 计算需要开辟的内存空间大小为何是16字节对齐

字节对齐算法(16字节为例)

两种16字节对齐算法:

1、alloc源码分析中的align16

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

以6+15为例的计算过程: 

(1).首先将原始的内存 6 与 size_t(15)相加,得到 6 + 15 = 21

(2).将 size_t(15) 即 15进行~(取反)操作,~(取反)的规则是:1变为0,0变为1

(3).最后将 21 与 15的取反结果 进行 &(与)操作,&(与)的规则是:都是1为1,反之为0,最后的结果为 16,即内存的大小是以16的倍数增加的

图解:

 2、malloc源码分析中的segregated_size_to_fit

#define SHIFT_NANO_QUANTUM      4
#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 + 15 >> 4 << 4 ,其中先右移4位再左移4位,相当于 k/16 * 16一样 ,是16字节对齐算法,小于16就成0了

图解6 + 15:​​​​​​​

 ​​​​​​​

 疑问:既然结构体中成员排序不同会导致其内存大小不一样,那么在OC中,编译后的代码里的结构体里的成员又是怎么排序的呢?编译器是否会对其进行优化呢?请看下一篇文章:iOS底层源码探究之内存优化(属性重排)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值