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é)保存了什么
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中保存的值
通過比對其他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方法返回的原始值
上圖的callq是調(diào)用0x100002700所在的函數(shù)脏榆。注釋寫的很清楚猖毫,這個函數(shù)名為Season.rawValue.getter
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對象的大小呢邪锌?
第一種方法是通過匯編查看勉躺,如圖。
或者我們可以直接通過開頭介紹的小工具觅丰,打印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)用方法的
關(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的
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()
結(jié)構(gòu)體由于不能繼承,其方法地址在編譯的時候就能確定逾礁。
0x100001a54 <+52>: callq 0x100001b20