我們先來問大家一個問題 下面打印結(jié)果是多少
var abc: Int = 100
var efg: Int? = 200
/*
Memlayout.size(ofValue value: T) // 獲取變量實際占用的內(nèi)存大小
Memlayout.stride(ofValue value: T) // 獲取創(chuàng)建變量所需要的分配的內(nèi)存大小
MemoryLayout.alignment(ofValue: T) // 獲取變量的內(nèi)存對齊數(shù)
*/
print("abc 實際占用的內(nèi)存 ======= \(MemoryLayout.size(ofValue: abc))")
print("efg 實際占用的內(nèi)存 ======= \(MemoryLayout.size(ofValue: efg))")
下面看一下打印結(jié)果
abc 實際占用的內(nèi)存 ======= 8
efg 實際占用的內(nèi)存 ======= 9
Program ended with exit code: 0
那么現(xiàn)在問題來了為什么可選類型(Int?)比不可選類型(Int)多一個字節(jié)?
那我們先來看一下可選類型代碼
@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
/// The absence of a value.
///
/// In code, the absence of a value is typically written using the `nil`
/// literal rather than the explicit `.none` enumeration case.
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
/// Creates an instance that stores the given value.
public init(_ some: Wrapped)
/// Creates an instance initialized with `nil`.
///
/// Do not call this initializer directly. It is used by the compiler when you
/// initialize an `Optional` instance with a `nil` literal. For example:
///
/// var i: Index? = nil
///
/// In this example, the assignment to the `i` variable calls this
/// initializer behind the scenes.
public init(nilLiteral: ())
}
查看Optional的源碼得知,其實在swift中可選類型就是一個添加了關(guān)聯(lián)值的枚舉,例如:
Int?`就等價于 `Optional<Int>
let age: Int? = 2` 就等價于 `let age: Optional<Int> = Optional.some(2)
ExpressibleByNilLiteral是一個nil的字面量協(xié)議,代表可以使用nil這個關(guān)鍵字來進(jìn)行初始化,Optional實現(xiàn)了這個協(xié)議的方法init(nilLiteral: ()) { self = .none },所以let age: Int? = nil就等價于 let age: Optional<Int> = Optional.init(nilLiteral: ())。
只是編譯器在背后幫我們做了一些轉(zhuǎn)換而已。
枚舉占多少內(nèi)存
1.1、普通枚舉
我們先來創(chuàng)建一個普通的枚舉用MemoryLayout獲取一下
enum Direction {
case north, south, east, west
}
enum Direction {
case north, south, east, west
}
func textEnum() {
let dir = Direction.south
print("Direction 實際占用內(nèi)存 ===== \(MemoryLayout<Direction>.size)")
print("dir 實際占用內(nèi)存 ===== \(MemoryLayout.size(ofValue: dir))")
}
// 打印結(jié)果
Direction 實際占用內(nèi)存 ===== 1
dir 實際占用內(nèi)存 ===== 1
Program ended with exit code: 0
那么現(xiàn)在我們知道了枚舉占用的內(nèi)存是一個字節(jié) 那么這一個字節(jié)里面裝的是什么呢?
通常如果我們知道了一個內(nèi)存地址,我們可以通過下面兩種方式查看地址對應(yīng)內(nèi)存空間存放的數(shù)據(jù):
1、我們可以在xcode -> Debug -> Debug workflow -> View Memory中輸入內(nèi)存地址定位到那塊內(nèi)存空間
2、在lldb中使用指令memory read + 內(nèi)存地址讀取指針對應(yīng)的內(nèi)存。也可以直接使用指令x簡化書寫,效果等同于memory read
現(xiàn)在問題就變成我們該如何獲取Swift變量的內(nèi)存地址了,但由于xcode對Swift語言做了非常多的封裝和屏蔽,斷點調(diào)試時,我們不能直接像oc/c語言那樣直接看到枚舉變量的地址,我們只能通過Swift的方式獲取內(nèi)存地址。
func getPointer<T>(of value: inout T) -> UnsafeRawPointer {
return withUnsafePointer(to: &value, { UnsafeRawPointer($0) })
}
我們來看一下內(nèi)存數(shù)據(jù)
func textEnum() {
var dir = Direction.south
print("\(getPointer(of: &dir))")
print("====================")
}

或者使用View Memory` 如下步驟

上面我們知道Direction枚舉只占用一個字節(jié),所以我們只需要查看第1個字節(jié)的數(shù)據(jù):可以看到原來dir變量在內(nèi)存中的真實存儲數(shù)據(jù)是0x1,同樣的我們也可以測試到Direction.north在內(nèi)存中的值是0x0,Direction.east在內(nèi)存中的值是0x2,Direction.west在內(nèi)存中的值是0x3。
?提醒:Swift和OC混編時,Swift中的enum要想在OC中使用,需要添加@objc修飾符,而添加完@objc修飾符之后,swift的枚舉占用的內(nèi)存大小就不是由枚舉類型的數(shù)目決定的了,而是固定為和Int類型大小一致。
1.2、帶初始值的枚舉
我們來看一下帶初始值的枚舉占多少內(nèi)存

枚舉的內(nèi)存還是一個字節(jié)存儲的是case的值 那么問題就來了那我們的初始值去哪了?
其實熟悉原始值使用語法的同學(xué)都知道,枚舉的原始值并不是直接拿來使用的,而是通過枚舉的一個名為rawValue的屬性才可以訪問到的,我們是不是可以根據(jù)剛才看到的內(nèi)存結(jié)構(gòu)大膽的猜測一下:是不是定義枚舉變量時,原始值并不會被存儲在枚舉的內(nèi)存空間中,而有可能只是編譯器幫我們生成了一個rawValue的計算屬性,然后在計算屬性的內(nèi)部判斷枚舉自身的類型來返回不同的原始值。
我們來看一下rawValue的匯編是什么樣的


通過rawValue的匯編可以看到是調(diào)用了rawValue的getter的函數(shù) 從這里可以猜測出計算屬性就是函數(shù)調(diào)用
給枚舉添加原始值就是編譯器幫我們實現(xiàn)了RawRepresentable協(xié)議,實現(xiàn)了rawValue、init(_ rawValue)函數(shù),rawValue函數(shù)在內(nèi)部對self參數(shù)進(jìn)行switch判斷,以此返回不同的的原始值。
帶關(guān)聯(lián)至的枚舉

可以看到三種類型的Achievement輸出的size都是9,為什么都是9個字節(jié)呢?
那么關(guān)聯(lián)值的實現(xiàn)是不是也可能像原始值那樣,是編譯器幫我們生成一些計算屬性、方法之類的,幫我們保存關(guān)聯(lián)值?仔細(xì)思考一下答案應(yīng)該是否定的,關(guān)聯(lián)值是不同于原始值的,因為原始值是一個確定值,在程序編譯時期就可以確定下來的值,而關(guān)聯(lián)值是不確定的,每一個枚舉變量綁定的關(guān)聯(lián)值都是不同的,值是在程序運行的時候才能確定的,我們可以使用case let語法從枚舉中解析出不同的關(guān)聯(lián)值, 那么這個關(guān)聯(lián)值一定是和枚舉變量有密切關(guān)聯(lián)的,所以關(guān)聯(lián)值是不是被直接存放在枚舉變量中呢?我們分析一下englishScore枚舉,一個Int類型在64位系統(tǒng)占用8個字節(jié),除此之外通過第一部分的學(xué)習(xí)我們知道枚舉自己還需要一個字節(jié)來區(qū)分枚舉類型,所以8 + 1 = 9,正好可以解釋ach變量的大小為什么是9。
大家會接著疑惑為什么ach1、ach2也占用9個字節(jié)呢?按照剛才的計算法則Bool變量只占1個字節(jié),加上枚舉自身的一個字節(jié)應(yīng)該是1 + 1 = 2個字節(jié)就可以了,為什么還需要占用9個字節(jié)呢?這個時候我們不能只考慮自身所占用的數(shù)據(jù)大小,大家想一下枚舉的一些使用場景,比如如果我們要將ach1重新賦值為其他的類型如ach1=Achievement.mathScore(100)`,ach1的兩個字節(jié)還夠用嗎,又比如我們定義一個枚舉數(shù)組,如果每一個元素的占用的內(nèi)存大小都不一樣,數(shù)組該怎么根據(jù)下標(biāo)尋址呢,所以枚舉枚舉變量的size是固定的,而大小是取決于需要占用內(nèi)存空間最大的那個類型。
1.3、默認(rèn)實現(xiàn)的協(xié)議
大家有沒有發(fā)現(xiàn)一個現(xiàn)象:我們定義的簡單枚舉類型<沒有關(guān)聯(lián)值>默認(rèn)就可以進(jìn)行==、!=運算,要知道在Swift語言中==不再是一個運算符了,==是一個函數(shù),是屬于Equatable協(xié)議中的一個函數(shù),但我們的枚舉又沒有實現(xiàn)Equatable怎么也可以進(jìn)行比較呢?
編譯器默認(rèn)會幫我們實現(xiàn)Hashable/Equatable協(xié)議,這就是為什么我們的枚舉可以調(diào)用hashValue屬性,可以進(jìn)行==運算的原因。接著我們給枚舉添加關(guān)聯(lián)值后再試一下,這個時候你會發(fā)現(xiàn)編譯器什么協(xié)議也沒幫我們添加,想必大家在開發(fā)過程中也發(fā)現(xiàn)了,設(shè)置關(guān)聯(lián)值之后的枚舉確實是不能進(jìn)行==運算的,大家猜想一下是為什么呢,為什么設(shè)置了關(guān)聯(lián)值編譯器就不幫我們實現(xiàn)協(xié)議了呢?
其實通過第二三部分的探索我們大概可以知道答案了,還是要從枚舉底層的內(nèi)存結(jié)構(gòu)來看,枚舉在沒有綁定關(guān)聯(lián)值的時候,本身其實就是一個整型值,類似Int,Swift系統(tǒng)的Int默認(rèn)也是實現(xiàn)了Hashable/Equatable協(xié)議的,系統(tǒng)當(dāng)然可以像對待Int一樣幫我們實現(xiàn)Hashable/Equatable協(xié)議,而當(dāng)我們添加了關(guān)聯(lián)值之后,枚舉在的內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)就是由枚舉本身和關(guān)聯(lián)值兩部分組成了,編譯器是不能確定具體要怎么樣比較,怎么樣hash了,則需要由我們開發(fā)者自己實現(xiàn)了。
1.4、總結(jié)
下面來總結(jié)一下我們學(xué)到的知識吧。
1、簡單枚舉<沒有關(guān)聯(lián)值>的本質(zhì)就是一個整型值,整型值的大小取決于該枚舉所定義的類型的數(shù)量。
2、給枚舉添加原始值不會影響枚舉自身的任何結(jié)構(gòu),設(shè)置原始值其實是編譯器幫我們添加了rawValue屬性,init(rawValue)方法(RawRepresentable協(xié)議)。
3、添加關(guān)聯(lián)值會影響枚舉內(nèi)存結(jié)構(gòu),關(guān)聯(lián)值被儲存在枚舉變量中,枚舉變量的大小取決于占用內(nèi)存最大的那個類型。
4、添加/調(diào)用"實例方法"、"類型方法"、計算屬性以及實現(xiàn)協(xié)議的本質(zhì)都是添加/調(diào)用函數(shù)。
5、對于沒有添加關(guān)聯(lián)值的枚舉系統(tǒng)會默認(rèn)幫我們實現(xiàn)Hashable/Equatable協(xié)議。