iOS-Swift-枚举变量的内存布局

枚举章节讲了下枚举,本文就详细分析枚举变量的内存布局。

创建命令行项目,执行如下代码:

var a = 10
print(a) //打断点

点击View Memory of "a",如下:

View Memory of "a"

可以发现变量a内存结构如下图:

a内存结构

A就是16进制的10,这种方式可以查看a的内存结构。

但是使用这种方式查看枚举变量的内存就是空,苹果不给我们查看枚举变量的内存结构。

一. 窥探枚举内存

1. 简单枚举

在命令行项目运行如下代码:

enum TestEnum {
    case test1, test2, test3
}

var t1 = TestEnum.test1
var t2 = TestEnum.test2
var t3 = TestEnum.test3
print("123")

枚举章节的学习我们知道了上面的枚举变量t1、t2、t3都是占用1字节,下面就通过窥探内存验证一下的确占用1字节。
但是如果使用文章刚开始的那种方式查看枚举变量的内存就是空,苹果不给我们查看枚举变量的内存结构,下面我们使用Mems.swift
打印枚举变量的内存地址,然后通过地址窥探内存布局。

运行代码:

enum TestEnum {
    case test1, test2, test3
}

var t1 = TestEnum.test1
var t2 = TestEnum.test2
var t3 = TestEnum.test3

print(Mems.ptr(ofVal: &t1))
print(Mems.ptr(ofVal: &t2))
print(Mems.ptr(ofVal: &t3))

print("123") //打断点

打印地址:

0x0000000100007928
0x0000000100007929
0x000000010000792a

点击Debug -> Debug Workflow -> View Memory,输入内存地址:0x0000000100007928

0x0000000100007928

可以发现前三个字节分别存储的是00 01 02,验证了上面的枚举的确占用一个字节,并且枚举变量在内存中直接存储的是0 1 2。

如果给上面枚举添加原始值:

enum TestEnum :Int {
    case test1 = 1, test2 = 2, test3 = 3
}

var t1 = TestEnum.test1
var t2 = TestEnum.test2
var t3 = TestEnum.test3

print(Mems.ptr(ofVal: &t1))
print(Mems.ptr(ofVal: &t2))
print(Mems.ptr(ofVal: &t3))

print("123") //打断点

同样的方式也可以验证,上面枚举变量占用的也是一个字节,并且枚举变量在内存中直接存储的也是0 1 2。

如果是添加关联值的枚举呢?

2. 带关联值的枚举

enum TestEnum {
    case test1(Int, Int, Int)
    case test2(Int, Int)
    case test3(Int)
    case test4(Bool)
    case test5
}

var e = TestEnum.test1(1, 2, 3)

MemoryLayout.size(ofValue: e) 
MemoryLayout.stride(ofValue: e) 
MemoryLayout.alignment(ofValue: e)

打印:

25
32
8

可以发现e实际占用25字节,系统分配了32字节,内存对齐为8。

下面通过查看内存看一下为什么是这样的?运行代码:

var e = TestEnum.test1(1, 2, 3)
print(Mems.ptr(ofVal: &e))

同样的方式,打印内存地址,查看内存:

 小端:高高低低
 01 00 00 00 00 00 00 00
 02 00 00 00 00 00 00 00
 03 00 00 00 00 00 00 00
 00
 00 00 00 00 00 00 00

补充:
CPU读取内存的方式分为大、小端模式,现在的CPU一般是小端模式,就是读数据的时候内存地址比较高的放在数据的高字节,内存地址比较低的放在数据的低字节,就是高高低低,所以 01 00 00 00 00 00 00 00读出来之后就是:0x00 00 00 00 00 00 00 01

可以看出前8个字节存放01,后8个字节存放02,再后面8个字节存放03,最后一个字节存放00。

当:e = .test2(4, 5)

 04 00 00 00 00 00 00 00
 05 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 01
 00 00 00 00 00 00 00

当:e = .test3(6)

 06 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 02
 00 00 00 00 00 00 00

当:e = .test4(true)

 01 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 03
 00 00 00 00 00 00 00

当:e = .test5

 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00
 04
 00 00 00 00 00 00 00

可能你已经看出规律了,上面枚举在内存中的存储方式是前24个字节分别存储枚举的关联值,后1个字节存储枚举的成员值。

总结:
1个字节存储成员值。
N个字节存储关联值(N取占用内存最大的关联值),任何一个case的关联值都共用这N个字节。

如果枚举中只有一个case呢?

enum TestEnum {
    case test
}

var e = TestEnum.test

MemoryLayout.size(ofValue: e) 
MemoryLayout.stride(ofValue: e) 
MemoryLayout.alignment(ofValue: e)

打印:

0
1
1

可以发现,e实际占用0字节,系统分配了1字节,内存对齐为1。
这个很好理解,因为就一个成员,编译器一看就知道肯定是test,根本不用管是哪个case,所以就不需要内存。

如果只有一个case并且有一个关联值呢?

enum TestEnum {
    case test(Int)
}

var e = TestEnum.test(10)

MemoryLayout.size(ofValue: e) 
MemoryLayout.stride(ofValue: e) 
MemoryLayout.alignment(ofValue: e)

打印:

8
8
8

这个也很好理解,因为只有一个case,所以根本不需要1字节来存储成员值,只需要用8个字节来存储关联值就好了。

作业:进一步观察下面枚举的内存布局

作业.png

二. Mems.swift的使用

上面我们都是打印地址,然后根据地址查看内存的,这样有点麻烦,其实Mems.swift也可以直接打印内存布局的

比如打印上面的test2(4, 5):

var e = TestEnum.test2(4, 5)
print(Mems.memStr(ofVal: &e))

打印:

0x00 00 00 00 00 00 00 04
0x00 00 00 00 00 00 00 05
0x00 00 00 00 00 00 00 00
0x00 00 00 00 00 00 00 01

默认是按照对齐方式来打印的,上面对齐是8,所以每8字节打印一次,如果想按其他方式打印,可传入参数,如下就是每2个字节打印一次:

print(Mems.memStr(ofVal: &e, alignment: .two))

三. 它们的switch语句底层又是如何实现的?

func testEnum() {
    enum TestEnum {
        case test1(Int, Int, Int)
        case test2(Int, Int)
        case test3(Int)
        case test4(Bool)
        case test5
    }
    
    var e = TestEnum.test1(10, 20, 30)  //打此处断点
    print(Mems.ptr(ofVal: &e))

    switch e {
    case let .test1(v1, v2, v3):
        print("test1", v1, v2, v3)
        
    case let .test2(v1, v2):
        print("test2", v1, v2)
        
    case let .test3(v1):
        print("test3", v1)
        
    case let .test4(v1):
        print("test4", v1)
        
    case .test5:
        print("test5")
    }
}

上面switch底层是怎么实现的呢?

  1. 当 switch e 的时候会先取出e的成员值
  2. 如果e的成员值是0,那么就匹配test1,然后就把枚举内存中的前8个字节(也就是10)赋值给v1,后8个字节(也就是20)赋值给v2,再后8个字节(也就是30)赋值给v3。
  3. 如果e的成员值是1,那么就匹配test2,然后把枚举内存中的前8个字节赋值给v1,后8个字节赋值给v2。
    ......
  4. 如果e的成员值是4,那么就匹配test5,直接打印。

结论我们知道了,下面通过查看汇编验证,验证之前不熟悉汇编的一定要先看一下汇编

执行上面代码,打开显示汇编,汇编代码如下:

->  0x100001c1f <+63>:   movq   $0xa, 0x5cde(%rip) //第一句
0x100001c2a <+74>:   leaq   0x5cd7(%rip), %rax  //第二句
0x100001c31 <+81>:   movq   $0x14, 0x5cd4(%rip)  //第三句
0x100001c3c <+92>:   movq   $0x1e, 0x5cd1(%rip)  //第四句
0x100001c47 <+103>:  movb   $0x0, 0x5cd2(%rip)  //第五句

如果看过汇编,上面的代码会很容易理解,这里我们一句一句解释。

第一句:将10赋值到内存地址:rip+0x5cde = 0x100001c2a + 0x5cde = 0x100007908,占用8字节
第二句:将0x5cd7(%rip)地址赋值给rax寄存器,0x5cd7(%rip) = 0x5cd7 + 0x100001c31 = 0x100007908
第三句:将20赋值到内存地址:rip+0x5cd4 = 0x100001c3c + 0x5cd4 = 0x100007910,占用8字节
第四句:将30赋值到内存地址:rip+0x5cd1 = 0x100001c47 + 0x5cd1 = 0x100007918,占用8字节
第五句:将0存储到内存地址:rip+0x5cd2 = 0x100001c4e + 0x5cd2 = 0x100007920,只占1个字节

总结:

  1. 这5句汇编就是var e = TestEnum.test1(10, 20, 30),只是简单的赋值,没函数调用
  2. 由于枚举的第一个成员的内存地址就是整个枚举变量的地址,所以上面的0x100007908就是枚举变量e的内存地址
  3. 第一、三、四句分别是将10、20、30存到内存中(也就是将枚举的关联值存入内存)
  4. 第五句是将0存到内存中(也就是将枚举的成员值存入内存)

补充:

0x100001c31 <+81>:   movq   $0x14, 0x5cd4(%rip)  //第三句
0x100001c3c <+92>:   movq   $0x1e, 0x5cd1(%rip)  //第四句

上面汇编,0x100001c3c代表这条汇编指令的内存地址,<+92>代表从函数最开始的0字节到这条指令中间有多少字节,通过这个数字我们可以算出上条指令占用多少字节,比如92 - 81 = 11,所以第三句指令占用11字节。

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