带你深入理解iOS-内存对齐

前言

  • 在iOS底层源码学习中,会需要分析一个结构体所占用的内存大小,这里面就涉及到了内存对齐
  • 今天,我将结合内存对齐的概念、原因、规则、实际例子,让你深入理解内存对齐,掌握分析结构体所占内存大小的方法。
    带你深入理解iOS-内存对齐_第1张图片
    目录.png
源码地址

MemoryAlignment

简介

内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。如果你想了解更加底层的秘密,探究“内存对齐”对你就不应该再模糊了。

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

特别对于我们学习底层源码,是需要掌握的知识点之一,下面我就结合百度百科-内存对齐以及实际的demo进行详细分析。

1、规则定义

  • 规则1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行

  • 规则2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行

  • 规则3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

2、规则解析

  • 规则1中表明数据成员的存放是按照定义的顺序依次存放的
  • #pragma pack是对齐系数,每个平台不一样,程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数(32位平台一般为4,64位平台一般为8)。iOS下默认为8。这个数值大家可以通过调试#pragma pack(n)测试验证得到。
  • 规则1,当第x(x>1)个成员y存放的时候,y按照min(n,m)来对齐存放,其中n为对齐系数,m为成员y的数据类型长度。
  • 在完成各个数据成员的存放排列后,通过规则2,取min(n,maxM)进行对齐,其中n为对齐系数,maxM为所有数据成员类型中长度的最大值。

3、实例分析

demo例子中我们定义具有相同类型和个数成员的结构体,但是其定义的顺序不同成员不同,并对其进行赋值,然后结合规则,详细进行分析。

基础知识点介绍

1.一个字节包含8个二进制位
2.一个十六进制位可用4个二进制位表示
3.一个字节可以由2个十六进制位表示
0x0000 0000 0000 0008表示16个16进制位,可以表示8个字节
所以8可以用8个字节0x0000 0000 0000 0008表示,或者4个字节0x0000 0008,或者2个字节0x0008,取决于定义8的数据类型。
字符'a'换成ASCII码为97,可以用 0x61表示。
此外,iOS系统的编译平台是按照小端法进行编译。

下面进入具体的实例分析,
环境:
Xcode 11.3.1,Deployment Target:10.15
代码如下:
CommandLineTool类型工程的main.m文件中


#import 

struct Person1 {
    char a;
    long b;
    int c;
    short d;
}MyPerson1;

struct Person2 {
    long b;
    char a;
    int c;
    short d;
}MyPerson2;

struct Person3 {
    long b;
    int c;
    char a;
    short d;
}MyPerson3;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MyPerson1.a = 'a';
        MyPerson1.b = 8;
        MyPerson1.c = 4;
        MyPerson1.d = 2;
        
        MyPerson2.b = 18;
        MyPerson2.a = 'a';
        MyPerson2.c = 14;
        MyPerson2.d = 12;
        
        MyPerson3.b = 28;
        MyPerson3.c = 24;
        MyPerson3.a = 'a';
        MyPerson3.d = 22;
        NSLog(@"Adress=======MyPerson1:%p,MyPerson2:%p,MyPerson3:%p",&MyPerson1,&MyPerson2,&MyPerson3);  
        NSLog(@"Size=======MyPerson1:%lu,MyPerson2:%lu,MyPerson3:%lu",sizeof(MyPerson1),sizeof(MyPerson2),sizeof(MyPerson3));

    }
    return 0;
}

分析MyPerson1

struct Person1 {
    char a;
    long b;
    int c;
    short d;
}MyPerson1;
  • 第一个成员char类型的成员a='a'占用1字节,此时:
    a: 0x61

  • 第二个成员long类型的成员b=8占用8个字节,根据规则解析3,b=8按照min(8,8)=8对齐,b的起始位置为8的倍数,不满足,a需要补齐7个字节保证b的起始位置为8的倍数
    此时:
    a:0x0000 0000 0000 0061
    b:0x0000 0000 0000 0008

  • 第三个成员int类型的成员c=4占用4个字节,根据规则解析3,整数c=4需要按照min(8,4)=4进行对齐,c的起始位置需要为4的整数倍,现在已经满足
    此时:
    a:0x0000 0000 0000 0061
    b:0x0000 0000 0000 0008
    c:0x0000 0004

  • 第四个成员short类型的整数d=2占用2个字节,根据规则解析3,d按照min(8,2)=2进行对齐,d的起始位置需要为2的整数倍,现在已经满足
    此时:
    a:0x0000 0000 0000 0061
    b:0x0000 0000 0000 0008
    c:0x0000 0004
    d:0x0002

  • 根据规则解析4,结构体需要进行整体对齐,取min(n,maxDataLength) = max(8,8) = 8对齐,现在为8+8+4+2=22字节,需要补2个字节,按照排列顺序,在d占用内存段补2个字节;

  • 最后得到
    a:0x0000 0000 0000 0061
    b:0x0000 0000 0000 0008
    c:0x0000 0004
    d:0000 0002
    其中我们看把c和d看成共占用一段8字节的内存,因为对齐系数为8,编译器按照8的整数倍来读取内存地址

  • 按照小端法进行修正,此时内存排列应该内应该是
    a:0x0000 0000 0000 0061
    b:0x0000 0000 0000 0008
    dc:0x0000 0002 0000 0004

    其中dc:0x0000 0002 0000 0004的第1-8位表示成员d的值,右边第9-16位表示成员c的值

  • 综上,MyPerson1结构体整体占用8+8+8=24字节

分析MyPerson2

struct Person2 {
    long b;
    char a;
    int c;
    short d;
}MyPerson2;

  • 第一个成员long类型的成员b=18占用8字节,此时:
    b:0x0000 0000 0000 0012

  • 第二个成员char类型的成员a='b'占用1个字节,根据规则解析3,a按照min(8,1)=1对齐,a的起始位置需要为1的整数倍,已经满足,此时:
    b:0x0000 0000 0000 0012
    a:0x62

  • 第三个成员int类型的成员c=14占用4个字节,根据规则解析3,c按照min(8,4)=4进行对齐,c的起始位置需要未4的整数倍,不满足,所以成员a='b'需要补齐3个字节, 此时:
    b:0x0000 0000 0000 0012
    a:0x0000 0062
    c:0x0000 000e

  • 第四个成员short类型的成员d=12占用2个字节,根据规则解析3,成员d按照min(8,2)=2进行对齐,起始位置需要为2的整数倍,已经满足,此时 :
    b:0x0000 0000 0000 0012
    a:0x0000 0062
    c:0x0000 000e
    d:000c

  • 根据规则解析4,结构体需要进行整体对齐,取min(n,maxDataLength) = max(8,8) = 8对齐, 现在占用8+4+4+2=18个字节,需要补6个字节,按照排列顺序,在d占用的内存段补6个字节

  • 最后得到 :
    b:0x0000 0000 0000 0012
    a:0x0000 0062
    c:0x0000 000e
    d:0000 0000 0000 000c,其中我们看把a、c看成共占用一段8字节的内存,因为对齐系数为8,编译器按照8的整数倍来读取内存地址

  • 按照小端法修正,此时真正的内存排列应该内应该是:
    b:0x0000 0000 0000 0012
    ca:0x0000 000e 0000 0062
    d:0x0000 0000 0000 000c ,

    其中ca:0x0x0000 000e 0000 0062 的第1-8位表示c的值,第9-16表示a的值

  • 综上,MyPerson2整体占用8+8+8=24个字节

分析MyPerson3

struct Person3 {
    long b;
    int c;
    char a;
    short d;
}MyPerson3;
  • 第一个成员long类型的成员b=28占用8字节,此时:
    b:0x0000 0000 0000 001c

  • 第二个成员int类型的成员c=24占用4个字节,根据规则1, 成员c按min(8,4)=4对齐,c的起始位置需要为4的整数倍,已经满足,此时:
    b:0x0000 0000 0000 001c
    c:0x0000 0018

  • 第三个成员char类型的成员a='c'占用1个字节,根据规则1,成员a按min(8,1)=1进行对齐此时:
    b:0x0000 0000 0000 001c
    c:0x0000 0018
    a:0x63

  • 第四个成员short类型的成员d=22占用2个字节,根据规则1,成员d按照min(8,2)=2进行对齐,d的起始位置需要为2的整数倍,因此成员a需要补1字节,此时:
    b:0x0000 0000 0000 001c
    c:0x0000 0018
    a:0x0063
    d:0x0016

  • 根据规则解析4,结构体需要进行整体对齐,取min(n,maxDataLength) = max(8,8) = 8对齐,但是现在占用8+4+2+2=16个字节,已经满足了

  • 最后得到:
    b:0x0000 0000 0000 001c
    c:0x0000 0018
    a:0063
    d:0016,其中我们看把c、a、d看成共占用一段8字节的内存,因为对齐系数为8,编译器按照8的整数倍来读取内存地址

  • 按照小端法修正,此时真正的内存排列应该内应该是 最后得到
    b:0x0000 0000 0000 001c
    dac:0x0016 0063 0000 0018
    ,其中dac:0x0016 0063 0000 0018的左边第1-4位表示d存储的值,左边第5-8位表示a存储的值,右边第9-16位表示c存储的值
    综上,MyPerson3结构体整体占用8+8=16个字节

验证

如图输出结构体成员信息


带你深入理解iOS-内存对齐_第2张图片
lldb输出.png
  • 我们把各个结构体的地址打印出来,然后利用lldb的x/4gx命令输出各个结构体里面的从第一个成员的起始位置开始的4段8字节内存信息
  • x/4gx 0x100002020表示打印从MyPerson1的成员a='a'开始的4段内存信息,其中前3段 0x0000000000000061 0x0000000000000008,0x0000000200000004和我们前面分析的MyPerson1得出的内存表示一致,最后一段0x0000000000000012不属于MyPerson1,代表MyPerson2的成员b=18内存表示
  • x/4gx 0x100002038表示打印从MyPerson2的成员b=18开始的4段内存信息,其中前3段 0x0000000000000012,0x0000000e00000062, 0x000000000000000c和我们前面分析的MyPerson2得出的内存表示一致,最后一段0x000000000000001c不属于MyPerson2,代表MyPerson3的成员b=28的内存表示
  • x/4gx 0x100002050 表示打印从MyPerson3的成员b=28开始的4段内存信息,其中前2段 0x000000000000001c,0x0016006300000018和我们前面分析的MyPerson2得出的内存表示一致,后面2段0x0000000000000000不属于MyPerson3

优化lldb的打印输出如下

带你深入理解iOS-内存对齐_第3张图片
优化输出
  • 通过优化输出可以看到lldb输出的内存表示与我们前面实例分析的是一致。

OC对象分析

仿照上面的3个结构体定义3个类Teacher1,Teacher2,Teacher3

@interface Teacher1 : NSObject

@property (nonatomic, assign) char a;
@property (nonatomic, assign) long b;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;

@end
@interface Teacher2 : NSObject

@property (nonatomic, assign) long b;
@property (nonatomic, assign) char a;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;

@end
@interface Teacher3 : NSObject

@property (nonatomic, assign) int c;
@property (nonatomic, assign) long b;
@property (nonatomic, assign) char a;
@property (nonatomic, assign) short d;

@end

main.m中添加如下代码

        Teacher1 *t1 = [[Teacher1 alloc] init];
        t1.a = 'a';
        t1.b = 8;
        t1.c = 4;
        t1.d = 2;
        
        Teacher2 *t2 = [[Teacher2 alloc] init];
        t2.b = 18;
        t2.a = 'b';
        t2.c = 14;
        t2.d = 12;
        
        Teacher3 *t3 = [[Teacher3 alloc] init];
        t3.b = 28;
        t3.c = 24;
        t3.a = 'c';
        t3.d = 22;
对象的输出如下
带你深入理解iOS-内存对齐_第4张图片
image.png
  • 可以看到,3个对象的第2个八字节和第三个八字节这2个内存段存储了我们定义的成员a、b、c、d(准确表述为_a,_b,_c,_d)的值,说明编译器做了相应的优化,不会直接按照我们在类中定义成员的顺序生成构造对应的结构体
  • 3个对象的第一个八字节存储着各自isa的值
  • 如果定义有float或者double类型的成员,比如Teacher1
@interface Teacher1 : NSObject

@property (nonatomic, assign) double height;

@end

Teacher1 *t1 = [[Teacher1 alloc] init];
t1.height = 175;

由于float和double的位表示是经过一定算法得到,无法直接通过简单手工计算得出,可以使用lldb命令:p/x (double)175得到的其位表示,再与x/4gx t1中的进行对比。

4、思考

下面代码输出什么?

NSLog(@"sizeof=======t1:%lu,t2:%lu,t3:%lu",sizeof(t1),sizeof(t2),sizeof(t3));
NSLog(@"class_getInstanceSize=======t1:%lu,t2:%lu,t3:%lu",class_getInstanceSize(t1.class),class_getInstanceSize(t2.class),class_getInstanceSize(t3.class));
NSLog(@"malloc_size=======t1:%lu,t2:%lu,t3:%lu",malloc_size((__bridge const void*)t1),malloc_size((__bridge const void*)t2),malloc_size((__bridge const void*)t3));

输出如下:

sizeof=======t1:8,t2:8,t3:8
class_getInstanceSize=======t1:24,t2:24,t3:24
malloc_size=======t1:32,t2:32,t3:32
  • sizeof计算的是传入参数的类型所占字节大小。t1为Teacher1 *类型,本质上是一个objc_objct的结构体指针,所以占8字节,t2、t3同理。
  • class_getInstanceSize输出的是对象实例经过内存对齐后的占用的大小。前面通过分析,t1所占大小应该为16字节,但是t1本质上是一个objc_objct的结构体指针,objc_objct结构体内部还有一个占8字节的isa_t,即我们通过lldb调试x/4gx输出的第一段内存,因此为8+16=24字节,t2、t3同理。
  • malloc_size返回的是指针所指向的内存空间所占的大小,即系统实际分配的大小。其值为class_getInstanceSize的值按16字节对齐得到,因此为32字节。

关于class_getInstanceSizemalloc_size的详解参考《OC底层系列二》-对象中的class_getInstanceSize以及malloc_size部分。

总结

  • 本文主要对内存对齐的规则进行介绍和并结合实际的demo例子对结构体和对象进行详细分析

你可能感兴趣的:(带你深入理解iOS-内存对齐)