Swift Enum底层探究

本文我们来探究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()
}

在这里插入图片描述

如果枚举变量dnil,函数dirMethod不会调用
如果枚举变量d不为nil,调用函数dirMethod前会将枚举变量d作为参数传入,从而实现了枚举变量和函数的关联

总结

  • 枚举变量只占一个字节,如果超过255种情况不应该使用枚举
  • 枚举的内存中存储的值依次是 0,1,… n-1 整数(n为枚举成员的数量)
  • 如果枚举变量为nil, 则内存中存储的值是n(n为枚举成员的数量)
  • 如果枚举的原始值,则编译器会自动生成计算属性rawValue和初始化方法init?(rawValue: T) -> ()
  • 枚举的关联值放在内存的前面,和枚举类型值放在一起存储(关联值的存储长度由最长的那个枚举成员决定)
  • 枚举的方法调用会将枚举变量作为参数传入,实现枚举和方法的关联

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...