本文我們來探究Swift
枚舉類型(Enum
)的底層實現(xiàn)邏輯。如果不想看分析過程就珠,可以直接看最后的總結材蛛。
如果對文中的匯編知識不清楚臊泌,可以查閱ARM64匯編入門這篇文章。
枚舉內存分析
- 枚舉的基本使用方法如下所示:
enum Direction {
case North
case South
case East
case West
}
- 枚舉的內存大小
let size = MemoryLayout<Direction>.size // 分配的內存大小是1字節(jié)
let stride = MemoryLayout<Direction>.stride // 實際使用的內存大小是1字節(jié)
let alignment = MemoryLayout<Direction>.alignment // 字節(jié)對齊是1
枚舉的內存大小是1個字節(jié)
- 匯編分析
<!-- 測試代碼 -->
func test() {
var direction = Direction.North
direction = Direction.South
direction = Direction.East
direction = Direction.West
}
<!-- 對應的匯編 -->
JJSwift`test():
0x100002be8 <+0>: sub sp, sp, #0x10
0x100002bec <+4>: strb wzr, [sp, #0xf] // 存儲的值是0
0x100002bf0 <+8>: mov w8, #0x1
0x100002bf4 <+12>: strb w8, [sp, #0xf] // 存儲的值是1
0x100002bf8 <+16>: mov w8, #0x2
0x100002bfc <+20>: strb w8, [sp, #0xf] // 存儲的值是2
0x100002c00 <+24>: mov w8, #0x3
0x100002c04 <+28>: strb w8, [sp, #0xf] // 存儲的值是3
0x100002c08 <+32>: add sp, sp, #0x10
0x100002c0c <+36>: ret
內存中存儲的值和數(shù)組的索引類似坏平,從0開始拢操,每個元素的存儲值+1
Direction.North : 0
Direction.South : 1
Direction.South : 2
Direction.West : 3
枚舉原始值
內存分析
枚舉可以使用相同類型(包括String
, Int
, Float
)的默認值和枚舉對應,這個默認值就是原始值(Raw Value)舶替。
- 使用方法
enum HttpMethod: String {
case GET // 編譯器默認綁定"GET"
case POST // 編譯器默認綁定"POST"
case PUT // 編譯器默認綁定"PUT"
case DELETE // 編譯器默認綁定"DELETE"
case PATCH = "OTHER" // 綁定"OTHER"
}
enum Direction: Int {
case North // 編譯器默認綁定0
case South // 編譯器默認綁定1
case East = 9 // 定9
case West // 編譯器默認綁定10
}
如果不指定原始值令境,編譯器會默認指定對應的原始值
- 枚舉的內存大小
let size = MemoryLayout<Direction>.size // 1
let stride = MemoryLayout<Direction>.stride // 1
let alignment = MemoryLayout<Direction>.alignment // 1
內存大小和未綁定原始值的情況一樣
- 匯編分析
enum Direction: Int {
case North // 編譯器默認綁定0
case South // 編譯器默認綁定1
case East = 9 // 定9
case West // 編譯器默認綁定10
}
<!-- 代碼 -->
func test() {
var direction = Direction.North
direction = Direction.South
direction = Direction.East
direction = Direction.West
}
<!-- 對應的匯編 -->
JJSwift`test():
0x100002be8 <+0>: sub sp, sp, #0x10
0x100002bec <+4>: strb wzr, [sp, #0xf] // 存儲的值是0
0x100002bf0 <+8>: mov w8, #0x1
0x100002bf4 <+12>: strb w8, [sp, #0xf] // 存儲的值是1
0x100002bf8 <+16>: mov w8, #0x2
0x100002bfc <+20>: strb w8, [sp, #0xf] // 存儲的值是2
0x100002c00 <+24>: mov w8, #0x3
0x100002c04 <+28>: strb w8, [sp, #0xf] // 存儲的值是3
0x100002c08 <+32>: add sp, sp, #0x10
0x100002c0c <+36>: ret
存儲值和未綁定原始值的情況一樣
獲取原始值
<!-- 代碼 -->
var raw = Direction.East.rawValue
編譯器自動生成了一個存儲屬性
rawValue
存儲屬性的實現(xiàn)邏輯:
如果枚舉內存值為0(即Direction.North):則返回 0
如果枚舉內存值為1(即Direction.South):則返回 1
如果枚舉內存值為2(即Direction.East):則返回 9
如果枚舉內存值為3(即Direction.West):則返回 10
- 編譯器等同于生成了以下代碼,可以獲取原始值
var rawValue: Int {
get {
switch self {
case .North:
return 0
case .South:
return 1
case .East:
return 9
case .West:
return 10
}
}
}
利用原始值初始化枚舉
<!-- 代碼 -->
var direction = Direction(rawValue: 10)
編譯器自動生成了一個
init
初始化方法
init的實現(xiàn)邏輯:
如果傳0顾瞪,返回枚舉Direction.North(內存值為0)
如果傳1舔庶,返回枚舉Direction.South(內存值為1)
如果傳9返劲,返回枚舉Direction.East(內存值為2)
如果傳10,返回枚舉Direction.West(內存值為3)
如果傳其他整形栖茉,返回nil(內存值為4)
- 編譯器等同于生成了以下代碼篮绿,進行枚舉的初始化
init?(rawValue: Int) {
switch rawValue {
case 0: self = .North
case 1: self = .South
case 9: self = .East
case 10: self = .West
default: return nil
}
}
枚舉可選項
init
方法返回的是枚舉可選項,如果是空存儲的值是4吕漂,如果直接定義一個枚舉可選項呢亲配?
func test() {
var dir: Direction?
}
JJSwift`test():
0x100002bd0 <+0>: sub sp, sp, #0x10
0x100002bd4 <+4>: mov w8, #0x4 // 直接賦值4
-> 0x100002bd8 <+8>: strb w8, [sp, #0xf]
0x100002bdc <+12>: add sp, sp, #0x10
0x100002be0 <+16>: ret
Direction?如果值為nil, 則內存中的值就確定是4
枚舉關聯(lián)值
每個枚舉項可以關聯(lián)一個值,這些值就是關聯(lián)值
enum Error {
case error1(Int, Int, Int)
case error2(Int, Int)
case error3(Int)
case error4
}
- 枚舉的內存大小
let size = MemoryLayout<Direction>.size // 25
let stride = MemoryLayout<Direction>.stride // 32
let alignment = MemoryLayout<Direction>.alignment // 8
只需要25個字節(jié)惶凝,實際占用了32個字節(jié)吼虎,8字節(jié)對齊
- 匯編和內存分析
<!-- 代碼 -->
func test() {
var error = Error.error1(1, 2, 3)
error = Error.error2(4, 5)
error = Error.error3(6)
error = Error.error4
}
<!-- 對應的匯編 -->
JJSwift`test():
0x100002608 <+0>: sub sp, sp, #0x20 // 壓棧
0x10000260c <+4>: mov w8, #0x1
0x100002610 <+8>: str x8, [sp] // 將1存入sp內存地址開始的8字節(jié)
0x100002614 <+12>: mov w8, #0x2
0x100002618 <+16>: str x8, [sp, #0x8] // 將2存入sp+0x8內存地址開始的8字節(jié)
0x10000261c <+20>: mov w8, #0x3
0x100002620 <+24>: str x8, [sp, #0x10] // 將3存入sp+0x10內存地址開始的8字節(jié)
0x100002624 <+28>: strb wzr, [sp, #0x18] // 將0存入sp+0x18內存地址開始的1字節(jié) (第一個變量內存賦值完成)
0x100002628 <+32>: mov w8, #0x4
0x10000262c <+36>: str x8, [sp]
0x100002630 <+40>: mov w8, #0x5
0x100002634 <+44>: str x8, [sp, #0x8]
0x100002638 <+48>: str xzr, [sp, #0x10]
0x10000263c <+52>: mov w8, #0x1
0x100002640 <+56>: strb w8, [sp, #0x18]
0x100002644 <+60>: mov w8, #0x6
0x100002648 <+64>: str x8, [sp]
0x10000264c <+68>: str xzr, [sp, #0x8]
0x100002650 <+72>: str xzr, [sp, #0x10]
0x100002654 <+76>: mov w8, #0x2
0x100002658 <+80>: strb w8, [sp, #0x18]
0x10000265c <+84>: str xzr, [sp]
0x100002660 <+88>: str xzr, [sp, #0x8]
0x100002664 <+92>: str xzr, [sp, #0x10]
0x100002668 <+96>: mov w8, #0x3
0x10000266c <+100>: strb w8, [sp, #0x18]
0x100002670 <+104>: add sp, sp, #0x20
0x100002674 <+108>: ret
我只分析了第一個變量的賦值:
Error.error1(1, 2, 3)
在內存中是1,2苍鲜,3思灰,0
,其他的類似混滔。
我們也可以直接查看下Error.error1(1, 2, 3)
內存的值:
(lldb) register read sp
sp = 0x000000016fdfdae0
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000001 0x0000000000000002
0x16fdfdaf0: 0x0000000000000003 0x00000001000d8000
內存中確實也是存儲的
1 2 3 0
(注意:0x00000001000d8000 只需要看最后一個字節(jié)洒疚,因為數(shù)據(jù)只存了一個字節(jié),其他的數(shù)據(jù)是臟數(shù)據(jù), 所以只占用了25個字節(jié))
<!-- Error.error1(1, 2, 3) -->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000001 0x0000000000000002
0x16fdfdaf0: 0x0000000000000003 0x00000001000d8000
<!--Error.error2(4, 5)-->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000004 0x0000000000000005
0x16fdfdaf0: 0x0000000000000000 0x00000001000d8001
<!--Error.error3(6)-->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000006 0x0000000000000000
0x16fdfdaf0: 0x0000000000000000 0x00000001000d8002
<!--error4-->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000000 0x0000000000000000
0x16fdfdaf0: 0x0000000000000000 0x00000001000d8003
枚舉的方法
Swift
的枚舉可以定義方法坯屿,我們看到枚舉變量的內存和方法是不相關的油湖,那枚舉變量是如何和方法關聯(lián)起來的呢?
enum Direction: Int {
case North
case South
case East = 9
case West
// 定義方法
func dirMethod() -> Int {
return 1
}
}
我們來看看方法調用的實現(xiàn):
func test() {
let d = Direction(rawValue: 9)
d?.dirMethod()
}
如果枚舉變量
d
為nil
领跛,函數(shù)dirMethod
不會調用
如果枚舉變量d
不為nil
乏德,調用函數(shù)dirMethod
前會將枚舉變量d
作為參數(shù)傳入,從而實現(xiàn)了枚舉變量和函數(shù)的關聯(lián)
總結
- 枚舉變量只占一個字節(jié)吠昭,如果超過255種情況不應該使用枚舉
- 枚舉的內存中存儲的值依次是 0喊括,1,... n-1 整數(shù)(n為枚舉成員的數(shù)量)
- 如果枚舉變量為
nil
, 則內存中存儲的值是n(n為枚舉成員的數(shù)量) - 如果枚舉的原始值矢棚,則編譯器會自動生成計算屬性
rawValue
和初始化方法init?(rawValue: T) -> ()
- 枚舉的關聯(lián)值放在內存的前面郑什,和枚舉類型值放在一起存儲(關聯(lián)值的存儲長度由最長的那個枚舉成員決定)
- 枚舉的方法調用會將枚舉變量作為參數(shù)傳入,實現(xiàn)枚舉和方法的關聯(lián)