我們先來(lái)問(wèn)大家一個(gè)問(wèn)題 下面打印結(jié)果是多少
var abc: Int = 100
var efg: Int? = 200
/*
Memlayout.size(ofValue value: T) // 獲取變量實(shí)際占用的內(nèi)存大小
Memlayout.stride(ofValue value: T) // 獲取創(chuàng)建變量所需要的分配的內(nèi)存大小
MemoryLayout.alignment(ofValue: T) // 獲取變量的內(nèi)存對(duì)齊數(shù)
*/
print("abc 實(shí)際占用的內(nèi)存 ======= \(MemoryLayout.size(ofValue: abc))")
print("efg 實(shí)際占用的內(nèi)存 ======= \(MemoryLayout.size(ofValue: efg))")
下面看一下打印結(jié)果
abc 實(shí)際占用的內(nèi)存 ======= 8
efg 實(shí)際占用的內(nèi)存 ======= 9
Program ended with exit code: 0
那么現(xiàn)在問(wèn)題來(lái)了為什么可選類型(Int眼虱?)比不可選類型(Int)多一個(gè)字節(jié)屉栓?
那我們先來(lái)看一下可選類型代碼
@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的源碼得知,其實(shí)在swift中可選類型就是一個(gè)添加了關(guān)聯(lián)值的枚舉衡楞,例如:
Int?`就等價(jià)于 `Optional<Int>
let age: Int? = 2` 就等價(jià)于 `let age: Optional<Int> = Optional.some(2)
ExpressibleByNilLiteral
是一個(gè)nil
的字面量協(xié)議,代表可以使用nil
這個(gè)關(guān)鍵字來(lái)進(jìn)行初始化,Optional
實(shí)現(xiàn)了這個(gè)協(xié)議的方法init(nilLiteral: ()) { self = .none }
图张,所以let age: Int? = nil
就等價(jià)于 let age: Optional<Int> = Optional.init(nilLiteral: ())
。
只是編譯器在背后幫我們做了一些轉(zhuǎn)換而已诈悍。
枚舉占多少內(nèi)存
1.1祸轮、普通枚舉
我們先來(lái)創(chuàng)建一個(gè)普通的枚舉用MemoryLayout獲取一下
enum Direction {
case north, south, east, west
}
enum Direction {
case north, south, east, west
}
func textEnum() {
let dir = Direction.south
print("Direction 實(shí)際占用內(nèi)存 ===== \(MemoryLayout<Direction>.size)")
print("dir 實(shí)際占用內(nèi)存 ===== \(MemoryLayout.size(ofValue: dir))")
}
// 打印結(jié)果
Direction 實(shí)際占用內(nèi)存 ===== 1
dir 實(shí)際占用內(nèi)存 ===== 1
Program ended with exit code: 0
那么現(xiàn)在我們知道了枚舉占用的內(nèi)存是一個(gè)字節(jié) 那么這一個(gè)字節(jié)里面裝的是什么呢?
通常如果我們知道了一個(gè)內(nèi)存地址侥钳,我們可以通過(guò)下面兩種方式查看地址對(duì)應(yīng)內(nèi)存空間存放的數(shù)據(jù):
1适袜、我們可以在xcode -> Debug -> Debug workflow -> View Memory
中輸入內(nèi)存地址定位到那塊內(nèi)存空間
2、在lldb中使用指令memory read + 內(nèi)存地址
讀取指針對(duì)應(yīng)的內(nèi)存舷夺。也可以直接使用指令x
簡(jiǎn)化書(shū)寫(xiě)苦酱,效果等同于memory read
現(xiàn)在問(wèn)題就變成我們?cè)撊绾潍@取Swift變量的內(nèi)存地址了,但由于xcode對(duì)Swift語(yǔ)言做了非常多的封裝和屏蔽给猾,斷點(diǎn)調(diào)試時(shí)疫萤,我們不能直接像oc/c語(yǔ)言那樣直接看到枚舉變量的地址,我們只能通過(guò)Swift的方式獲取內(nèi)存地址敢伸。
func getPointer<T>(of value: inout T) -> UnsafeRawPointer {
return withUnsafePointer(to: &value, { UnsafeRawPointer($0) })
}
我們來(lái)看一下內(nèi)存數(shù)據(jù)
func textEnum() {
var dir = Direction.south
print("\(getPointer(of: &dir))")
print("====================")
}
或者使用View Memory` 如下步驟
上面我們知道Direction枚舉只占用一個(gè)字節(jié)扯饶,所以我們只需要查看第1個(gè)字節(jié)的數(shù)據(jù):可以看到原來(lái)dir變量在內(nèi)存中的真實(shí)存儲(chǔ)數(shù)據(jù)是0x1,同樣的我們也可以測(cè)試到Direction.north在內(nèi)存中的值是0x0详拙,Direction.east在內(nèi)存中的值是0x2帝际,Direction.west在內(nèi)存中的值是0x3。
?提醒:Swift和OC混編時(shí)饶辙,Swift中的enum要想在OC中使用蹲诀,需要添加@objc修飾符,而添加完@objc修飾符之后弃揽,swift的枚舉占用的內(nèi)存大小就不是由枚舉類型的數(shù)目決定的了脯爪,而是固定為和Int類型大小一致则北。
1.2、帶初始值的枚舉
我們來(lái)看一下帶初始值的枚舉占多少內(nèi)存
枚舉的內(nèi)存還是一個(gè)字節(jié)存儲(chǔ)的是case的值 那么問(wèn)題就來(lái)了那我們的初始值去哪了痕慢?
其實(shí)熟悉原始值使用語(yǔ)法的同學(xué)都知道尚揣,枚舉的原始值并不是直接拿來(lái)使用的,而是通過(guò)枚舉的一個(gè)名為rawValue
的屬性才可以訪問(wèn)到的掖举,我們是不是可以根據(jù)剛才看到的內(nèi)存結(jié)構(gòu)大膽的猜測(cè)一下:是不是定義枚舉變量時(shí)快骗,原始值并不會(huì)被存儲(chǔ)在枚舉的內(nèi)存空間中,而有可能只是編譯器幫我們生成了一個(gè)rawValue
的計(jì)算屬性塔次,然后在計(jì)算屬性的內(nèi)部判斷枚舉自身的類型來(lái)返回不同的原始值方篮。
我們來(lái)看一下rawValue的匯編是什么樣的
通過(guò)rawValue的匯編可以看到是調(diào)用了rawValue的getter的函數(shù) 從這里可以猜測(cè)出計(jì)算屬性就是函數(shù)調(diào)用
給枚舉添加原始值就是編譯器幫我們實(shí)現(xiàn)了RawRepresentable
協(xié)議,實(shí)現(xiàn)了rawValue
励负、init(_ rawValue)
函數(shù)藕溅,rawValue
函數(shù)在內(nèi)部對(duì)self
參數(shù)進(jìn)行switch
判斷,以此返回不同的的原始值继榆。
帶關(guān)聯(lián)至的枚舉
可以看到三種類型的Achievement輸出的size都是9巾表,為什么都是9個(gè)字節(jié)呢?
那么關(guān)聯(lián)值的實(shí)現(xiàn)是不是也可能像原始值那樣略吨,是編譯器幫我們生成一些計(jì)算屬性集币、方法之類的,幫我們保存關(guān)聯(lián)值晋南?仔細(xì)思考一下答案應(yīng)該是否定的惠猿,關(guān)聯(lián)值是不同于原始值的羔砾,因?yàn)樵贾凳且粋€(gè)確定值负间,在程序編譯時(shí)期就可以確定下來(lái)的值,而關(guān)聯(lián)值是不確定的姜凄,每一個(gè)枚舉變量綁定的關(guān)聯(lián)值都是不同的政溃,值是在程序運(yùn)行的時(shí)候才能確定的,我們可以使用case let
語(yǔ)法從枚舉中解析出不同的關(guān)聯(lián)值态秧, 那么這個(gè)關(guān)聯(lián)值一定是和枚舉變量有密切關(guān)聯(lián)的董虱,所以關(guān)聯(lián)值是不是被直接存放在枚舉變量中呢?我們分析一下englishScore枚舉申鱼,一個(gè)Int類型在64位系統(tǒng)占用8個(gè)字節(jié)愤诱,除此之外通過(guò)第一部分的學(xué)習(xí)我們知道枚舉自己還需要一個(gè)字節(jié)來(lái)區(qū)分枚舉類型,所以8 + 1 = 9
捐友,正好可以解釋ach變量的大小為什么是9淫半。
大家會(huì)接著疑惑為什么ach1、ach2也占用9個(gè)字節(jié)呢匣砖?按照剛才的計(jì)算法則Bool
變量只占1個(gè)字節(jié)科吭,加上枚舉自身的一個(gè)字節(jié)應(yīng)該是1 + 1 = 2
個(gè)字節(jié)就可以了昏滴,為什么還需要占用9個(gè)字節(jié)呢?這個(gè)時(shí)候我們不能只考慮自身所占用的數(shù)據(jù)大小对人,大家想一下枚舉的一些使用場(chǎng)景谣殊,比如如果我們要將ach1重新賦值為其他的類型如ach1=Achievement.mathScore(100)`,ach1的兩個(gè)字節(jié)還夠用嗎牺弄,又比如我們定義一個(gè)枚舉數(shù)組姻几,如果每一個(gè)元素的占用的內(nèi)存大小都不一樣,數(shù)組該怎么根據(jù)下標(biāo)尋址呢势告,所以枚舉枚舉變量的size是固定的鲜棠,而大小是取決于需要占用內(nèi)存空間最大的那個(gè)類型。
1.3培慌、默認(rèn)實(shí)現(xiàn)的協(xié)議
大家有沒(méi)有發(fā)現(xiàn)一個(gè)現(xiàn)象:我們定義的簡(jiǎn)單枚舉類型<沒(méi)有關(guān)聯(lián)值>默認(rèn)就可以進(jìn)行==
豁陆、!=
運(yùn)算,要知道在Swift語(yǔ)言中==
不再是一個(gè)運(yùn)算符了吵护,==
是一個(gè)函數(shù)盒音,是屬于Equatable
協(xié)議中的一個(gè)函數(shù),但我們的枚舉又沒(méi)有實(shí)現(xiàn)Equatable
怎么也可以進(jìn)行比較呢馅而?
編譯器默認(rèn)會(huì)幫我們實(shí)現(xiàn)Hashable/Equatable
協(xié)議祥诽,這就是為什么我們的枚舉可以調(diào)用hashValue
屬性,可以進(jìn)行==
運(yùn)算的原因瓮恭。接著我們給枚舉添加關(guān)聯(lián)值后再試一下雄坪,這個(gè)時(shí)候你會(huì)發(fā)現(xiàn)編譯器什么協(xié)議也沒(méi)幫我們添加,想必大家在開(kāi)發(fā)過(guò)程中也發(fā)現(xiàn)了屯蹦,設(shè)置關(guān)聯(lián)值之后的枚舉確實(shí)是不能進(jìn)行==
運(yùn)算的维哈,大家猜想一下是為什么呢,為什么設(shè)置了關(guān)聯(lián)值編譯器就不幫我們實(shí)現(xiàn)協(xié)議了呢登澜?
其實(shí)通過(guò)第二三部分的探索我們大概可以知道答案了阔挠,還是要從枚舉底層的內(nèi)存結(jié)構(gòu)來(lái)看,枚舉在沒(méi)有綁定關(guān)聯(lián)值的時(shí)候脑蠕,本身其實(shí)就是一個(gè)整型值购撼,類似Int
,Swift系統(tǒng)的Int
默認(rèn)也是實(shí)現(xiàn)了Hashable/Equatable
協(xié)議的谴仙,系統(tǒng)當(dāng)然可以像對(duì)待Int
一樣幫我們實(shí)現(xiàn)Hashable/Equatable
協(xié)議迂求,而當(dāng)我們添加了關(guān)聯(lián)值之后,枚舉在的內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)就是由枚舉本身和關(guān)聯(lián)值兩部分組成了晃跺,編譯器是不能確定具體要怎么樣比較揩局,怎么樣hash了,則需要由我們開(kāi)發(fā)者自己實(shí)現(xiàn)了哼审。
1.4谐腰、總結(jié)
下面來(lái)總結(jié)一下我們學(xué)到的知識(shí)吧孕豹。
1、簡(jiǎn)單枚舉<沒(méi)有關(guān)聯(lián)值>的本質(zhì)就是一個(gè)整型值十气,整型值的大小取決于該枚舉所定義的類型的數(shù)量励背。
2、給枚舉添加原始值不會(huì)影響枚舉自身的任何結(jié)構(gòu)砸西,設(shè)置原始值其實(shí)是編譯器幫我們添加了rawValue
屬性叶眉,init(rawValue)
方法(RawRepresentable協(xié)議
)。
3芹枷、添加關(guān)聯(lián)值會(huì)影響枚舉內(nèi)存結(jié)構(gòu)衅疙,關(guān)聯(lián)值被儲(chǔ)存在枚舉變量中,枚舉變量的大小取決于占用內(nèi)存最大的那個(gè)類型鸳慈。
4饱溢、添加/調(diào)用"實(shí)例方法"、"類型方法"走芋、計(jì)算屬性以及實(shí)現(xiàn)協(xié)議的本質(zhì)都是添加/調(diào)用函數(shù)绩郎。
5、對(duì)于沒(méi)有添加關(guān)聯(lián)值的枚舉系統(tǒng)會(huì)默認(rèn)幫我們實(shí)現(xiàn)Hashable/Equatable
協(xié)議翁逞。