Swift5中的枚舉存皂、結(jié)構(gòu)體和類在內(nèi)存中的布局

swift查看內(nèi)存地址小工具Mems
https://github.com/CoderMJLee/Mems.git

1.枚舉

1.1 觀察枚舉所占內(nèi)存情況
enum Season{
    case spring,
         summer,
         autumn,
         winter,
         unknown
}
var s = Season.autumn
var val = s.rawValue
// s占用了多少字節(jié)
print("s占用了" + "\(MemoryLayout.size(ofValue: s))" + "字節(jié)")
// 系統(tǒng)實際給s分配了多少個字節(jié)
print("系統(tǒng)實際給s分配了" + "\(MemoryLayout.stride(ofValue: s))" + "字節(jié)")
// 內(nèi)存對齊的字節(jié)數(shù)長度
print("內(nèi)存對齊的字節(jié)長度" + "\(MemoryLayout.alignment(ofValue: s))" + "字節(jié)")
系統(tǒng)實際給s分配了1字節(jié)
s實際占用了1字節(jié)
內(nèi)存對齊的字節(jié)長度1字節(jié)
Program ended with exit code: 0

在沒有原始值和關(guān)聯(lián)值的情況下泻帮,枚舉只占用一個字節(jié)悠抹。我們可以通過匯編查看這一個字節(jié)保存了什么


Snip20190722_1.png

AT&T匯編中MOV為賦值指令夭织,MOV后面的字母為操作數(shù)長度网杆,b(byte)為一個字節(jié)吃环。$代表著字面量也颤,%開頭的是CPU的寄存器。 movb $0x2, 0x500f(%rip)這一句匯編代碼的意思就是將2這個常量賦值給寄存器%rip中的地址加上0x500f(也就是枚舉變量S的地址)郁轻。寄存器中%rip保存的是下一條匯編代碼的地址翅娶,所以0x500f(%rip)=0x100001579 + 0x500f = 0x100006588文留。
通過xcode查看0x100006588中保存的值


Snip20190722_2.png

通過比對其他4個枚舉在內(nèi)存中的值,可以發(fā)現(xiàn)枚舉在內(nèi)存中的值和定義順序有關(guān)竭沫。
spring為0燥翅,summer為1,autumn為2蜕提,winter為3森书,unkonwn為4,且占用內(nèi)存為1個字節(jié)谎势。
1.2 原始值是如何影響枚舉內(nèi)存的
enum Season:String{
    case spring = "spring",
         summer = "summer",
         autumn = "autumn",
         winter = "winter",
         unknown = "unknown"
}

var s = Season.autumn
var val = s.rawValue
// s占用了多少字節(jié)
print("s占用了" + "\(MemoryLayout.size(ofValue: s))" + "字節(jié)")
// 系統(tǒng)實際給s分配了多少個字節(jié)
print("系統(tǒng)實際給s分配了" + "\(MemoryLayout.stride(ofValue: s))" + "字節(jié)")
// 內(nèi)存對齊的字節(jié)數(shù)長度
print("內(nèi)存對齊的字節(jié)長度" + "\(MemoryLayout.alignment(ofValue: s))" + "字節(jié)")
系統(tǒng)實際給s分配了1字節(jié)
s實際占用了1字節(jié)
內(nèi)存對齊的字節(jié)長度1字節(jié)
Program ended with exit code: 0

觀察打印結(jié)果凛膏,我們發(fā)現(xiàn)原始值并不會影響枚舉內(nèi)存的大小,通過匯編可以窺探到swift是通過了rawValue這個計算屬性的getter方法返回的原始值


Snip20190723_1.png

上圖的callq是調(diào)用0x100002700所在的函數(shù)脏榆。注釋寫的很清楚猖毫,這個函數(shù)名為Season.rawValue.getter


Snip20190723_2.png

getter方法的內(nèi)部使用了一個switch語句,根據(jù)case來判斷應(yīng)該跳轉(zhuǎn)返回什么原始值须喂。
jmpq *%rdx 代表著跳轉(zhuǎn)到%rdx中代碼段的地址吁断,而rdx是根據(jù)枚舉值等一系列計算得出的,意味著會根據(jù)枚舉值執(zhí)行相應(yīng)的switch代碼段返回對應(yīng)的原始值坞生。

1.3 關(guān)聯(lián)值是如何影響枚舉內(nèi)存的
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占用了多少字節(jié)
print("spring占用了" + "\(MemoryLayout.size(ofValue: spring))" + "字節(jié)")
// 系統(tǒng)實際給spring分配了多少個字節(jié)
print("系統(tǒng)實際給spring分配了" + "\(MemoryLayout.stride(ofValue: spring))" + "字節(jié)")
// 內(nèi)存對齊的字節(jié)數(shù)長度
print("內(nèi)存對齊的字節(jié)長度" + "\(MemoryLayout.alignment(ofValue: spring))" + "字節(jié)")
spring占用了49字節(jié)
系統(tǒng)實際給spring分配了56字節(jié)
內(nèi)存對齊的字節(jié)長度8字節(jié)
//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占用了多少字節(jié)
print("summer占用了" + "\(MemoryLayout.size(ofValue: summer))" + "字節(jié)")
// 系統(tǒng)實際給summer分配了多少個字節(jié)
print("系統(tǒng)實際給summer分配了" + "\(MemoryLayout.stride(ofValue: summer))" + "字節(jié)")
// 內(nèi)存對齊的字節(jié)數(shù)長度
print("內(nèi)存對齊的字節(jié)長度" + "\(MemoryLayout.alignment(ofValue: summer))" + "字節(jié)")
summer占用了49字節(jié)
系統(tǒng)實際給summer分配了56字節(jié)
內(nèi)存對齊的字節(jié)長度8字節(jié)
//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占用了多少字節(jié)
print("autumn占用了" + "\(MemoryLayout.size(ofValue: autumn))" + "字節(jié)")
// 系統(tǒng)實際給summer分配了多少個字節(jié)
print("系統(tǒng)實際給autumn分配了" + "\(MemoryLayout.stride(ofValue: autumn))" + "字節(jié)")
// 內(nèi)存對齊的字節(jié)數(shù)長度
print("內(nèi)存對齊的字節(jié)長度" + "\(MemoryLayout.alignment(ofValue: autumn))" + "字節(jié)")
autumn占用了49字節(jié)
系統(tǒng)實際給autumn分配了56字節(jié)
內(nèi)存對齊的字節(jié)長度8字節(jié)

觀察打印結(jié)果仔役,可以發(fā)現(xiàn)枚舉的內(nèi)存大小受關(guān)聯(lián)值的影響,也就是說枚舉的關(guān)聯(lián)值是存儲在枚舉內(nèi)部的是己。
比如 Spring(Int,Int,Int)一個Int在x86架構(gòu)下占8個字節(jié)又兵,那么其3個Int類型的關(guān)聯(lián)值就是24個字節(jié),還要加上一個字節(jié)存放枚舉值赃泡,所以是25個字節(jié),又因為內(nèi)存對齊的長度是8個字節(jié)乘盼,系統(tǒng)分配給Spring這個枚舉的內(nèi)存應(yīng)當(dāng)是8的倍數(shù)升熊,所以會分配32個字節(jié)給Spring。但是打印結(jié)果告訴我們Spring占用了49個字節(jié)绸栅,系統(tǒng)實際分配了56個字節(jié)级野,這是為什么呢?
因為枚舉值分配的空間是按照最大的枚舉值來分配的粹胯,例子中Season類型的枚舉summer(String,String,String)需要占用49個字節(jié)(一個Stirng占16個字節(jié)蓖柔,3 * 16 + 1 = 49),所以Season會給所有的枚舉值分配49個字節(jié)风纠,并在第49個字節(jié)存放枚舉值况鸣。由于內(nèi)存對齊長度為8個字節(jié),系統(tǒng)分配的內(nèi)存必須為8的倍數(shù)竹观。所以系統(tǒng)會分配56個字節(jié)給Season類型的枚舉值镐捧。

單個枚舉所占空間是按照枚舉關(guān)聯(lián)值所占字節(jié)總和最高的枚舉字節(jié)數(shù)+1個字節(jié)的方式來分配的潜索。

枚舉結(jié)語

在沒有關(guān)聯(lián)值的情況下,枚舉在內(nèi)存中占1個字節(jié)且所占內(nèi)存的大小不受原始值影響懂酱。原始值以計算屬性的方式存在枚舉中竹习,調(diào)用rawValue屬性會通過switch的方式返回相應(yīng)的原始值。關(guān)聯(lián)值會保存在枚舉的內(nèi)存中列牺,影響著枚舉所占內(nèi)存的大小整陌。說到這里我們也可以明白為什么枚舉不能定義存儲屬性了,因為枚舉中一旦保存存儲屬性會和枚舉的關(guān)聯(lián)值產(chǎn)生歧義瞎领。

2.類

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

var animal = Animal.init()
// 查看animal對象所占內(nèi)存
print(MemoryLayout.stride(ofValue: animal))
// 查看animal對象實際所占內(nèi)存
print(MemoryLayout.size(ofValue: animal))
// 查看animal對象內(nèi)存對齊的字節(jié)數(shù)長度
print(MemoryLayout.alignment(ofValue: animal))

打印結(jié)果

8
8
8
Program ended with exit code: 0

無論往Person對象中增加還是減少存儲屬性泌辫,通過MemoryLayout類方法打印出的內(nèi)存占用都是8個字節(jié),這是因為Animal對象存儲在堆中默刚,animal變量存儲在全局區(qū)甥郑,animal變量內(nèi)部保存著Animal對象的內(nèi)存地址,MemoryLayout打印的是animal這個變量所占用的內(nèi)存荤西,所以無論如何打印出來的都是swift指針大小澜搅,也就是8個字節(jié)。

那我們?nèi)绾尾榭碅nimal對象的大小呢邪锌?

第一種方法是通過匯編查看勉躺,如圖。


Snip20190723_1.png

或者我們可以直接通過開頭介紹的小工具觅丰,打印Animal對象的地址

print(Mems.ptr(ofRef: animal))

打印結(jié)果

0x100708730

Animal對象實際占用24個字節(jié)饵溅,由于堆空間內(nèi)存對齊的長度為16個字節(jié),意味著Animal對象占用的內(nèi)存必須為16的倍數(shù)妇萄,所以系統(tǒng)實際給Animal對象分配了32個字節(jié)
98 65 00 00 01 00 00 00 類型信息
02 00 00 00 00 00 00 00 引用計數(shù)信息(此處的引用計數(shù)是系統(tǒng)通過對該值計算得出的蜕企,而不是保存的2)
0A 00 00 00 00 00 00 00 age變量
10 90 45 57 FF 7F 00 00 堆空間的內(nèi)存對齊長度為16個字節(jié),此8個字節(jié)為臟數(shù)據(jù)

通過內(nèi)存數(shù)據(jù)觀察冠句,我們可以很直觀的看到第17~24個字節(jié)保存著age變量轻掩。

但是如何證明前8個字節(jié)是類型信息,第9~16個字節(jié)保存的是引用計數(shù)呢懦底?

我們先來證明第9~16個字節(jié)保存的是引用計數(shù)
我們用四個變量指向Animal對象唇牧,并打上斷點,觀察每一個增加一個變量指向Aniaml對象后聚唐,Animal對象的內(nèi)存是如何變化的丐重。

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

// 1個變量指向Aniaml對象,Animal對象第9~16個字節(jié)存儲的數(shù)據(jù)
02 00 00 00 00 00 00 00
// 2個變量指向Aniaml對象杆查,Animal對象第9~16個字節(jié)存儲的數(shù)據(jù)
02 00 00 00 02 00 00 00
// 3個變量指向Aniaml對象扮惦,Animal對象第9~16個字節(jié)存儲的數(shù)據(jù)
02 00 00 00 04 00 00 00
// 4個變量指向Aniaml對象,Animal對象第9~16個字節(jié)存儲的數(shù)據(jù)
02 00 00 00 06 00 00 00

通過內(nèi)存觀察亲桦,我們發(fā)現(xiàn)径缅,Animal對象的第9~16個字節(jié)隨著指向其變量的增加密切變動掺栅,可以證明其存儲的數(shù)據(jù)和引用計數(shù)相關(guān)。

我們接著來證明1~8個字節(jié)保存的是類的類型信息纳猪,為了看得更清楚一些氧卧,我寫了一個子類繼承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對象是如何調(diào)用方法的


Snip20190723_3.png

關(guān)鍵匯編代碼如下:
通過匯編注釋我們很明顯的發(fā)現(xiàn) 0x57df(%rip)這個地址保存的值是Animal對象的前8個字節(jié)氏堤,movq指令意味著將%rip寄存器中保存的地址加上0x57df后得到一個新的地址并將第1~8個字節(jié)的值賦值給了寄存器%rax沙绝。

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

然后又將%rax的值賦值給了%rcx

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

最后將%rcx的值加上0x78,得出一個函數(shù)地址值鼠锈,并且調(diào)用這個函數(shù)闪檬。通過前面的分析我們知道%rcx中保存的其實就是animal對象的前8個字節(jié),animal.breath()的函數(shù)地址值是根據(jù)animal對象前8個字節(jié)的值的偏移0x78得出的购笆,animal.eat()的函數(shù)地址值是根據(jù)animal對象前8個字節(jié)的值的偏移0x80得出的粗悯。

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

我們再來看下當(dāng)animal指針指向Animal的子類對象Person后,子類對象Person是如何調(diào)用父類Animal的對象方法breath和eat的


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

此時%rcx中保存的其實就是Person對象的前8個字節(jié)同欠,Person.breath()的函數(shù)地址值是根據(jù)Person對象前8個字節(jié)的值的偏移0x78得出的样傍,Person.eat()的函數(shù)地址值是根據(jù)Person對象前8個字節(jié)的值的偏移0x80得出的医寿。
由此得出泛释,class的對象的前8個字節(jié)保存著type的meta data,其中包括了方法的地址悲关。

類結(jié)語

類的結(jié)構(gòu)相比結(jié)構(gòu)體要復(fù)雜一些襟锐,由于類的實例對象保存在堆空間中撤逢,系統(tǒng)需要通過檢查引用計數(shù)的情況來確定是否需要回收對象(ARC中系統(tǒng)已經(jīng)幫我們處理堆內(nèi)存的管理,程序員不需要關(guān)心引用計數(shù)粮坞,但這并不代表引用計數(shù)不存在)蚊荣,所以對象中需要留出8個字節(jié)保存引用計數(shù)情況。類可以被繼承莫杈,由于面向?qū)ο笳Z言的多態(tài)特性互例,在調(diào)用類的實例對象方法時,編譯器需要動態(tài)地獲取對象方法所在的函數(shù)地址姓迅,所以需要留出8個字節(jié)保存類的類型信息敲霍,比如對象方法的地址就保存在類型信息中俊马。
所以當(dāng)類的實例對象在調(diào)用對象方法時丁存,性能的開銷相比結(jié)構(gòu)體以及枚舉調(diào)用方法要大,因為多態(tài)的存在柴我,系統(tǒng)會先找到該對象的前8個字節(jié)(type meta data)加上一個偏移值得到函數(shù)的地址解寝,再找到這個函數(shù)去調(diào)用。

3.結(jié)構(gòu)體

3.1 觀察結(jié)構(gòu)體所占內(nèi)存情況

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

let per = Person()
// 查看結(jié)構(gòu)體per所占內(nèi)存
print(MemoryLayout.stride(ofValue: per))
// 查看結(jié)構(gòu)體per實際所占內(nèi)存
print(MemoryLayout.size(ofValue: per))
// 查看結(jié)構(gòu)體per內(nèi)存對齊的字節(jié)數(shù)長度
print(MemoryLayout.alignment(ofValue: per))

打印結(jié)果

16
9
8
Program ended with exit code: 0

由于結(jié)構(gòu)體是值類型艘儒,相較于類而言其不能被子類繼承聋伦,也不需要引用計數(shù)來管理其內(nèi)存的釋放。所以在存儲屬性相同的情況下觉增,結(jié)構(gòu)體的內(nèi)存要比類小兵拢。再來看一下結(jié)構(gòu)體的方法調(diào)用

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

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

結(jié)構(gòu)體由于不能繼承,其方法地址在編譯的時候就能確定逾礁。

0x100001a54 <+52>: callq  0x100001b20  
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末说铃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嘹履,更是在濱河造成了極大的恐慌腻扇,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砾嫉,死亡現(xiàn)場離奇詭異幼苛,居然都是意外死亡,警方通過查閱死者的電腦和手機焕刮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門舶沿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人济锄,你說我怎么就攤上這事暑椰。” “怎么了荐绝?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵一汽,是天一觀的道長。 經(jīng)常有香客問我低滩,道長召夹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任恕沫,我火速辦了婚禮监憎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘婶溯。我一直安慰自己鲸阔,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布迄委。 她就那樣靜靜地躺著褐筛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叙身。 梳的紋絲不亂的頭發(fā)上渔扎,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音信轿,去河邊找鬼晃痴。 笑死残吩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的倘核。 我是一名探鬼主播泣侮,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼紧唱!你這毒婦竟也來了旁瘫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤琼蚯,失蹤者是張志新(化名)和其女友劉穎酬凳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遭庶,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡宁仔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了峦睡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翎苫。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖榨了,靈堂內(nèi)的尸體忽然破棺而出煎谍,到底是詐尸還是另有隱情,我是刑警寧澤龙屉,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布呐粘,位于F島的核電站,受9級特大地震影響转捕,放射性物質(zhì)發(fā)生泄漏作岖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一五芝、第九天 我趴在偏房一處隱蔽的房頂上張望痘儡。 院中可真熱鬧,春花似錦枢步、人聲如沸沉删。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矾瑰。三九已至,卻和暖如春结蟋,著一層夾襖步出監(jiān)牢的瞬間脯倚,已是汗流浹背渔彰。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工嵌屎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留推正,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓宝惰,卻偏偏與公主長得像植榕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子尼夺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容