探索结构体内存对齐

在探讨结构体内存对齐原理之前,首先介绍下iOS中获取内存大小的三种方式

获取内存大小的三种方式

获取内存大小的三种方式分别是: sizeof,class_getInstanceSize,malloc_size

sizeof

  • sizeof是一个操作符,不是函数
  • 我们一般用 sizeof 计算内存大小时,传入的主要对象是数据类型,这个在编译器的编译阶段(即编译时)就会确定大小而不是在运行时确定。
  • sizeof 最终得到的结果是该数据类型占用空间的大小

class_getInstanceSize

这个方法是runtime提供的api,用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小

malloc_size

这个函数是获取系统实际分配的内存大小

可以通过下面代码的输出结果来验证我们上面的说法

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

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

以下是打印结果

三种获取内存大小的打印结果

总结

  • sizeof:计算类型占用的内存大小,其中可以放 基本数据类型、对象、指针

    • 对于int这样的基本数据而言,sizeof 获取的就是数据类型占用的内存大小,不同的数据类型所占用的内存大小是不一样的

    • 对于WJPerson 定义的实例对象而言,其对象类型的本质就是一个结构体(即 struct objc_object)的指针,所以sizeof(person)打印的是对象person指针大小,我们知道一个指针的内存大小是8,所以sizeof(person) 打印是 8。注意:这里的8字节isa指针一点关系都没有!!!)

    • 对于指针而言,sizeof打印的就是8,因为一个指针内存大小是8

  • class_getInstanceSize:计算对象实际占用的内存大小,这个需要依据类的属性而变化,如果自定义类没有自定义属性,仅仅只是继承自NSObject,则类的实例对象实际占用的内存大小是8,可以简单理解为8字节对齐

  • malloc_size:计算对象实际分配的内存大小,这个是由系统完成的,可以从上面的打印结果看出,实际分配的和实际占用的内存大小并不相等

结构体内存对齐

接下来,我们首先定义两个结构体,分别计算他们的内存大小,以此来引入今天的正题:结构体内存对齐原理


struct WJStruct1 {
    double a;       
    char b;        
    int c;          
    short d;       
}struct1;

struct WJStruct2 {
    double a;       
    int b;         
    char c;         
    short d;      
}struct2;

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

复制代码

打印结果如下:
结构体占用的内存大小

从打印结果我们可以看出一个问题,两个结构体乍一看,没什么区别,其中定义的变量变量类型都是一致的,唯一的区别只是在于定义变量的顺序不一致,那为什么他们占用的内存大小不相等呢?其实这就是iOS中的内存字节对齐现象

内存对齐原则

可以将内存对齐原则可以理解为以下几点:

  • 【原则一】 数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), nm 位置开始存储, 反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置
  • 【原则二】数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体bb中有char、int、double等,则b的自身长度为8
  • 【原则三】最后结构体的内存大小必须是结构体中最大成员内存大小整数倍,不足的需要补齐。

验证对齐规则

下表是各种数据类型iOS中的占用内存大小,根据对应类型来计算结构体中内存大小

数据类型对应的字节数表格

我们可以通过下图来说明下为什么两个结构体 WJStruct1 & WJStruct2的内存大小打印不一致的情况,如图所示

image.png

  • 根据内存对齐规则得出WJStruct1的内存大小是18 ,但是18不是最大变量的字节数8的整数倍,18向上取整到24,主要是因为248的整数倍,所以 sizeof(struct1) 的结果是 24
  • 根据内存对齐规则得出WJStruct2的内存大小是16 ,16刚好是8的整数倍,所以sizeof(struct2) 的结果是 16

结构体嵌套结构体

上面的两个结构体只是简单的定义数据成员,下面来一个比较复杂的,结构体中嵌套结构体的内存大小计算情况

首先定义一个结构体WJStruct3,在WJStruct3中嵌套WJStruct1,如下所示

struct WJStruct3 {
    double a;              //8
    int b;                 //4
    char c;                //1
    short d;               //2
    int e;                 //4
    struct WJStruct1 str;  //24
}struct3;

//打印 WJStruct3 的内存大小
NSLog(@"struct3内存大小:%lu", sizeof(struct3));
NSLog(@"struct3中结构体成员内存大小:%lu", sizeof(struct3.str));

复制代码

打印结果如下:
image.png

根据内存对齐规则,我们可以画WJStruct3的内存分布图:

image.png

分析WJStruct3的内存计算:

  • 变量adouble类型8个字节,从0开始,此时min(0,8),即 0-7 存储 a
  • 变量bint类型4个字节,从8开始,此时min(8,4)8可以整除4,即 8-11 存储 b
  • 变量cchar类型1个字节,从12开始,此时min(12, 1)12可以整除1,即12 存储 c
  • 变量dshort类型2个字节,从13开始,此时min(13,2),不能整除,+1后可以整除,即 14-15 存储 d
  • 变量eint类型4个字节,从16开始,此时min(16,4)16可以整除416-19 存储 e
  • 结构体成员strstr 是一个结构体,根据内存对齐原则二,结构体成员要从其内部最大成员大小的整数倍开始存储,而WJStruct2中最大的成员大小为8,所以str要从8的整数倍开始,当前是从20开始,所以不符合要求,需要往后移动到24248的整数倍,符合内存对齐原则,所以 24-47 存储 str

因此WJStruct3的需要的内存大小为 48 字节,而 WJStruct3 中最大变量为str, 其最大成员内存字节数为8,根据内存对齐原则,所以 WJStruct3 实际的内存大小必须是 8 的整数倍,48正好是8的整数倍,所以 sizeof(WJStruct3) 的结果是 48

内存优化(属性重排)

WJStruct1 通过内存字节对齐原则,增加了8个字节,而 WJStruct2 通过内存字节对齐原则,通过4+1+2的组合,只需要补齐一个字节即可满足字节对齐规则,这里得出一个结论结构体内存大小与结构体成员内存大小的顺序有关

如果是结构体中数据成员是根据内存从小到大的顺序定义的,根据内存对齐规则来计算结构体内存大小,需要增加有较大的内存padding即内存占位符,才能满足内存对齐规则,比较浪费内存

如果是结构体中数据成员是根据内存从大到小的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足堆存对齐规则,这种方式就是苹果中采用的,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的

以下面这个例子来进行说明 苹果中属性重排,即内存优化

  • 定义一个自定义 WJPerson 类,并定义几个属性
@interface WJPerson : NSObject


@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@property (nonatomic) char c1;
@property (nonatomic) char c2;

@end
复制代码
  • main 中创建 WJPerson 的实例对象,并对其中的几个属性赋值
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        WJPerson *person = [WJPerson alloc];
        person.name      = @"Jem";
        person.nickName  = @"WJ";
        person.age       = 18;
        person.c1        = 'a';
        person.c2        = 'b';
        NSLog(@"%@",person);
    }
    return 0;
}
复制代码
  • 断点调试person,根据WJPerson的对象地址,查找出属性的值

image.png

当我们向通过0x0000001200006261地址找出age等数据时,发现读不到,这里无法找出值的原因是苹果中针对age、c1、c2属性的内存进行了重排,因为age类型占4个字节,c1c2类型char分别占1个字节,通过4+1+1的方式,按照8字节对齐,不足补齐的方式存储在同一块内存中,

  • age 的读取通过 po 0x00000012(18)
  • c1 的读取通过 x/c 0x61(a)
  • c2 的读取通过 x/c 0x62(b)

总结

所以,这里可以总结下苹果中的内存对齐思想:

  • 大部分的内存都是通过固定的内存块进行读取,

  • 尽管我们在内存中采用了内存对齐的方式,但并不是所有的内存都可以进行浪费的,苹果会自动对属性进行重排,以此来优化内存

  • 字节对齐到底采用多少字节对齐?我们可以通过objc4的源码来进行分析

    • class_getInstanceSize:是采用8字节对齐,参照的对象的属性内存大小
    • malloc_size:采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍
  • 内存对齐算法原理:k + 15 >> 4 << 4 ,其中 右移4 + 左移4相当于将后4位抹零,跟 k/16 * 16一样 ,是16字节对齐算法,小于16就成0了.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享