本文我们来探究Swift
枚举类型(Enum
)的底层实现逻辑。如果不想看分析过程,可以直接看最后的总结。
如果对文中的汇编知识不清楚,可以查阅ARM64汇编入门这篇文章。
枚举内存分析
- 枚举的基本使用方法如下所示:
enum Direction {
case North
case South
case East
case West
}
- 枚举的内存大小
let size = MemoryLayout<Direction>.size // 分配的内存大小是1字节
let stride = MemoryLayout<Direction>.stride // 实际使用的内存大小是1字节
let alignment = MemoryLayout<Direction>.alignment // 字节对齐是1
枚举的内存大小是1个字节
- 汇编分析
<!-- 测试代码 -->
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
内存中存储的值和数组的索引类似,从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
存储属性的实现逻辑:
如果枚举内存值为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的实现逻辑:
如果传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
枚举关联值
每个枚举项可以关联一个值,这些值就是关联值
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个字节,实际占用了32个字节,8字节对齐
- 汇编和内存分析
<!-- 代码 -->
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字节
0x100002614 <+12>: mov w8, #0x2
0x100002618 <+16>: str x8, [sp, #0x8] // 将2存入sp+0x8内存地址开始的8字节
0x10000261c <+20>: mov w8, #0x3
0x100002620 <+24>: str x8, [sp, #0x10] // 将3存入sp+0x10内存地址开始的8字节
0x100002624 <+28>: strb wzr, [sp, #0x18] // 将0存入sp+0x18内存地址开始的1字节 (第一个变量内存赋值完成)
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 只需要看最后一个字节,因为数据只存了一个字节,其他的数据是脏数据, 所以只占用了25个字节)
<!-- 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
的枚举可以定义方法,我们看到枚举变量的内存和方法是不相关的,那枚举变量是如何和方法关联起来的呢?
enum Direction: Int {
case North
case South
case East = 9
case West
// 定义方法
func dirMethod() -> Int {
return 1
}
}
我们来看看方法调用的实现:
func test() {
let d = Direction(rawValue: 9)
d?.dirMethod()
}
如果枚举变量
d
为nil
,函数dirMethod
不会调用
如果枚举变量d
不为nil
,调用函数dirMethod
前会将枚举变量d
作为参数传入,从而实现了枚举变量和函数的关联
总结
- 枚举变量只占一个字节,如果超过255种情况不应该使用枚举
- 枚举的内存中存储的值依次是 0,1,... n-1 整数(n为枚举成员的数量)
- 如果枚举变量为
nil
, 则内存中存储的值是n(n为枚举成员的数量) - 如果枚举的原始值,则编译器会自动生成计算属性
rawValue
和初始化方法init?(rawValue: T) -> ()
- 枚举的关联值放在内存的前面,和枚举类型值放在一起存储(关联值的存储长度由最长的那个枚举成员决定)
- 枚举的方法调用会将枚举变量作为参数传入,实现枚举和方法的关联