Swift5中的枚举、结构体和类在内存中的布局

swift查看内存地址小工具Mems
https://github.com/CoderMJLee/Mems.git

1.枚举

1.1 观察枚举所占内存情况
enum Season{
    case spring,
         summer,
         autumn,
         winter,
         unknown
}
var s = Season.autumn
var val = s.rawValue
// s占用了多少字节
print("s占用了" + "\(MemoryLayout.size(ofValue: s))" + "字节")
// 系统实际给s分配了多少个字节
print("系统实际给s分配了" + "\(MemoryLayout.stride(ofValue: s))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: s))" + "字节")
系统实际给s分配了1字节
s实际占用了1字节
内存对齐的字节长度1字节
Program ended with exit code: 0

在没有原始值和关联值的情况下,枚举只占用一个字节。我们可以通过汇编查看这一个字节保存了什么


Snip20190722_1.png

AT&T汇编中MOV为赋值指令,MOV后面的字母为操作数长度,b(byte)为一个字节。$代表着字面量,%开头的是CPU的寄存器。 movb $0x2, 0x500f(%rip)这一句汇编代码的意思就是将2这个常量赋值给寄存器%rip中的地址加上0x500f(也就是枚举变量S的地址)。寄存器中%rip保存的是下一条汇编代码的地址,所以0x500f(%rip)=0x100001579 + 0x500f = 0x100006588。
通过xcode查看0x100006588中保存的值


Snip20190722_2.png

通过比对其他4个枚举在内存中的值,可以发现枚举在内存中的值和定义顺序有关。
spring为0,summer为1,autumn为2,winter为3,unkonwn为4,且占用内存为1个字节。
1.2 原始值是如何影响枚举内存的
enum Season:String{
    case spring = "spring",
         summer = "summer",
         autumn = "autumn",
         winter = "winter",
         unknown = "unknown"
}

var s = Season.autumn
var val = s.rawValue
// s占用了多少字节
print("s占用了" + "\(MemoryLayout.size(ofValue: s))" + "字节")
// 系统实际给s分配了多少个字节
print("系统实际给s分配了" + "\(MemoryLayout.stride(ofValue: s))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: s))" + "字节")
系统实际给s分配了1字节
s实际占用了1字节
内存对齐的字节长度1字节
Program ended with exit code: 0

观察打印结果,我们发现原始值并不会影响枚举内存的大小,通过汇编可以窥探到swift是通过了rawValue这个计算属性的getter方法返回的原始值


Snip20190723_1.png

上图的callq是调用0x100002700所在的函数。注释写的很清楚,这个函数名为Season.rawValue.getter


Snip20190723_2.png

getter方法的内部使用了一个switch语句,根据case来判断应该跳转返回什么原始值。
jmpq *%rdx 代表着跳转到%rdx中代码段的地址,而rdx是根据枚举值等一系列计算得出的,意味着会根据枚举值执行相应的switch代码段返回对应的原始值。

1.3 关联值是如何影响枚举内存的
enum Season{
    case spring(Int,Int,Int),
         summer(String,String,String),
         autumn(Bool,Bool,Bool),
         winter(Int,Int),
         unknown(Bool)
}
//01 00 00 00 00 00 00 00
//01 00 00 00 00 00 00 00
//01 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
var spring = Season.spring(1, 1, 1)
// s占用了多少字节
print("spring占用了" + "\(MemoryLayout.size(ofValue: spring))" + "字节")
// 系统实际给spring分配了多少个字节
print("系统实际给spring分配了" + "\(MemoryLayout.stride(ofValue: spring))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: spring))" + "字节")
spring占用了49字节
系统实际给spring分配了56字节
内存对齐的字节长度8字节
//61 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 E1
//61 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 E1
//61 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 E1
//01 00 00 00 00 00 00 00
var summer = Season.summer("a", "a", "a")
// summer占用了多少字节
print("summer占用了" + "\(MemoryLayout.size(ofValue: summer))" + "字节")
// 系统实际给summer分配了多少个字节
print("系统实际给summer分配了" + "\(MemoryLayout.stride(ofValue: summer))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: summer))" + "字节")
summer占用了49字节
系统实际给summer分配了56字节
内存对齐的字节长度8字节
//01 01 01 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 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
var autumn = Season.autumn(true, true, true)
// summer占用了多少字节
print("autumn占用了" + "\(MemoryLayout.size(ofValue: autumn))" + "字节")
// 系统实际给summer分配了多少个字节
print("系统实际给autumn分配了" + "\(MemoryLayout.stride(ofValue: autumn))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: autumn))" + "字节")
autumn占用了49字节
系统实际给autumn分配了56字节
内存对齐的字节长度8字节

观察打印结果,可以发现枚举的内存大小受关联值的影响,也就是说枚举的关联值是存储在枚举内部的。
比如 Spring(Int,Int,Int)一个Int在x86架构下占8个字节,那么其3个Int类型的关联值就是24个字节,还要加上一个字节存放枚举值,所以是25个字节,又因为内存对齐的长度是8个字节,系统分配给Spring这个枚举的内存应当是8的倍数,所以会分配32个字节给Spring。但是打印结果告诉我们Spring占用了49个字节,系统实际分配了56个字节,这是为什么呢?
因为枚举值分配的空间是按照最大的枚举值来分配的,例子中Season类型的枚举summer(String,String,String)需要占用49个字节(一个Stirng占16个字节,3 * 16 + 1 = 49),所以Season会给所有的枚举值分配49个字节,并在第49个字节存放枚举值。由于内存对齐长度为8个字节,系统分配的内存必须为8的倍数。所以系统会分配56个字节给Season类型的枚举值。

单个枚举所占空间是按照枚举关联值所占字节总和最高的枚举字节数+1个字节的方式来分配的。

枚举结语

在没有关联值的情况下,枚举在内存中占1个字节且所占内存的大小不受原始值影响。原始值以计算属性的方式存在枚举中,调用rawValue属性会通过switch的方式返回相应的原始值。关联值会保存在枚举的内存中,影响着枚举所占内存的大小。说到这里我们也可以明白为什么枚举不能定义存储属性了,因为枚举中一旦保存存储属性会和枚举的关联值产生歧义。

2.类

class Animal{
    var age:Int = 0
    var height:Int = 10
//    var weight:Int = 20
    init() {
    }
}

var animal = Animal.init()
// 查看animal对象所占内存
print(MemoryLayout.stride(ofValue: animal))
// 查看animal对象实际所占内存
print(MemoryLayout.size(ofValue: animal))
// 查看animal对象内存对齐的字节数长度
print(MemoryLayout.alignment(ofValue: animal))

打印结果

8
8
8
Program ended with exit code: 0

无论往Person对象中增加还是减少存储属性,通过MemoryLayout类方法打印出的内存占用都是8个字节,这是因为Animal对象存储在堆中,animal变量存储在全局区,animal变量内部保存着Animal对象的内存地址,MemoryLayout打印的是animal这个变量所占用的内存,所以无论如何打印出来的都是swift指针大小,也就是8个字节。

那我们如何查看Animal对象的大小呢?

第一种方法是通过汇编查看,如图。


Snip20190723_1.png

或者我们可以直接通过开头介绍的小工具,打印Animal对象的地址

print(Mems.ptr(ofRef: animal))

打印结果

0x100708730

Animal对象实际占用24个字节,由于堆空间内存对齐的长度为16个字节,意味着Animal对象占用的内存必须为16的倍数,所以系统实际给Animal对象分配了32个字节
98 65 00 00 01 00 00 00 类型信息
02 00 00 00 00 00 00 00 引用计数信息(此处的引用计数是系统通过对该值计算得出的,而不是保存的2)
0A 00 00 00 00 00 00 00 age变量
10 90 45 57 FF 7F 00 00 堆空间的内存对齐长度为16个字节,此8个字节为脏数据

通过内存数据观察,我们可以很直观的看到第17~24个字节保存着age变量。

但是如何证明前8个字节是类型信息,第9~16个字节保存的是引用计数呢?

我们先来证明第9~16个字节保存的是引用计数
我们用四个变量指向Animal对象,并打上断点,观察每一个增加一个变量指向Aniaml对象后,Animal对象的内存是如何变化的。

var animal = Animal.init()
var animal1 = animal
var animal2 = animal
var animal3 = animal
print(Mems.ptr(ofRef: animal))

// 1个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 00 00 00 00
// 2个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 02 00 00 00
// 3个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 04 00 00 00
// 4个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 06 00 00 00

通过内存观察,我们发现,Animal对象的第9~16个字节随着指向其变量的增加密切变动,可以证明其存储的数据和引用计数相关。

我们接着来证明1~8个字节保存的是类的类型信息,为了看得更清楚一些,我写了一个子类继承Animal,代码如下:

class Animal{
    var age:Int = 10
    init() {}
    func breath() {
        print("animal breath")
    }
    func eat() {
        print("animal eat")
    }
}

class Person:Animal{
    var name:String?
    override func breath() {
        print("person breath")
    }
    override func eat() {
        print("person eat")
    }
}

var animal:Animal = Animal.init()
print(Mems.ptr(ofRef: animal))
animal.breath()
animal.eat()
animal = Person()
print(Mems.ptr(ofRef: animal))
animal.breath()
animal.eat()

我们通过汇编观察Animal对象是如何调用方法的


Snip20190723_3.png

关键汇编代码如下:
通过汇编注释我们很明显的发现 0x57df(%rip)这个地址保存的值是Animal对象的前8个字节,movq指令意味着将%rip寄存器中保存的地址加上0x57df后得到一个新的地址并将第1~8个字节的值赋值给了寄存器%rax。

 0x100001022 <+418>:  movq   0x57df(%rip), %rax        ; command.animal : command.Animal

然后又将%rax的值赋值给了%rcx

 0x100001029 <+425>:  movq   %rax, %rcx

最后将%rcx的值加上0x78,得出一个函数地址值,并且调用这个函数。通过前面的分析我们知道%rcx中保存的其实就是animal对象的前8个字节,animal.breath()的函数地址值是根据animal对象前8个字节的值的偏移0x78得出的,animal.eat()的函数地址值是根据animal对象前8个字节的值的偏移0x80得出的。

0x100001058 <+472>:  callq  *0x78(%rcx) // animal.breath() 
0x1000010bf <+575>:  callq  *0x80(%rcx) //  animal.eat()

我们再来看下当animal指针指向Animal的子类对象Person后,子类对象Person是如何调用父类Animal的对象方法breath和eat的


Snip20190724_6.png
0x1000012f2 <+1138>: callq  *0x78(%rcx) // person.breath()
0x10000135f <+1247>: callq  *0x80(%rcx) // person.eat()

此时%rcx中保存的其实就是Person对象的前8个字节,Person.breath()的函数地址值是根据Person对象前8个字节的值的偏移0x78得出的,Person.eat()的函数地址值是根据Person对象前8个字节的值的偏移0x80得出的。
由此得出,class的对象的前8个字节保存着type的meta data,其中包括了方法的地址。

类结语

类的结构相比结构体要复杂一些,由于类的实例对象保存在堆空间中,系统需要通过检查引用计数的情况来确定是否需要回收对象(ARC中系统已经帮我们处理堆内存的管理,程序员不需要关心引用计数,但这并不代表引用计数不存在),所以对象中需要留出8个字节保存引用计数情况。类可以被继承,由于面向对象语言的多态特性,在调用类的实例对象方法时,编译器需要动态地获取对象方法所在的函数地址,所以需要留出8个字节保存类的类型信息,比如对象方法的地址就保存在类型信息中。
所以当类的实例对象在调用对象方法时,性能的开销相比结构体以及枚举调用方法要大,因为多态的存在,系统会先找到该对象的前8个字节(type meta data)加上一个偏移值得到函数的地址,再找到这个函数去调用。

3.结构体

3.1 观察结构体所占内存情况

struct Person {
    var age:Int = 10
    var man:Bool = true
    func test() {
        print("test")
    }
}

let per = Person()
// 查看结构体per所占内存
print(MemoryLayout.stride(ofValue: per))
// 查看结构体per实际所占内存
print(MemoryLayout.size(ofValue: per))
// 查看结构体per内存对齐的字节数长度
print(MemoryLayout.alignment(ofValue: per))

打印结果

16
9
8
Program ended with exit code: 0

由于结构体是值类型,相较于类而言其不能被子类继承,也不需要引用计数来管理其内存的释放。所以在存储属性相同的情况下,结构体的内存要比类小。再来看一下结构体的方法调用

struct Person {
    var age:Int = 10
    var man:Bool = true
    func test() {
        print("test")
    }
}

let per = Person()
per.test()
Snip20190724_1.png

结构体由于不能继承,其方法地址在编译的时候就能确定。

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

推荐阅读更多精彩内容