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

前言

  • 在iOS底层源码学习中,会需要分析一个结构体所占用的内存大小,这里面就涉及到了内存对齐
  • 今天,我将结合内存对齐的概念、原因、规则、实际例子,让你深入理解内存对齐,掌握分析结构体所占内存大小的方法。
    目录.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 <Foundation/Foundation.h>

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个字节

验证

如图输出结构体成员信息


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的打印输出如下

优化输出
  • 通过优化输出可以看到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;
对象的输出如下
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例子对结构体和对象进行详细分析
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,000评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,825评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,055评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,766评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,639评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,453评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,860评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,492评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,783评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,817评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,624评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,439评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,872评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,075评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,372评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,868评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,075评论 2 338