• Swift Enum底层探究


    本文我们来探究Swift枚举类型(Enum)的底层实现逻辑。如果不想看分析过程,可以直接看最后的总结。
    如果对文中的汇编知识不清楚,可以查阅ARM64汇编入门这篇文章。

    枚举内存分析
    • 枚举的基本使用方法如下所示:
    enum Direction {
        case North
        case South
        case East
        case West
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 枚举的内存大小
    let size = MemoryLayout.size              // 分配的内存大小是1字节
    let stride = MemoryLayout.stride          // 实际使用的内存大小是1字节
    let alignment = MemoryLayout.alignment    // 字节对齐是1
    
    • 1
    • 2
    • 3

    枚举的内存大小是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    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    内存中存储的值和数组的索引类似,从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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如果不指定原始值,编译器会默认指定对应的原始值

    • 枚举的内存大小
    let size = MemoryLayout.size              // 1
    let stride = MemoryLayout.stride          // 1
    let alignment = MemoryLayout.alignment    // 1
    
    • 1
    • 2
    • 3

    内存大小和未绑定原始值的情况一样

    • 汇编分析
    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    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    存储值和未绑定原始值的情况一样

    获取原始值
    
    var raw = Direction.East.rawValue
    
    • 1
    • 2

    在这里插入图片描述

    编译器自动生成了一个存储属性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
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    利用原始值初始化枚举
    
    var direction = Direction(rawValue: 10)
    
    • 1
    • 2

    在这里插入图片描述

    编译器自动生成了一个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
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    枚举可选项

    init方法返回的是枚举可选项,如果是空存储的值是4,如果直接定义一个枚举可选项呢?

    func test() {
        var dir: Direction?
    }
    
    • 1
    • 2
    • 3
    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    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Direction?如果值为nil, 则内存中的值就确定是4

    枚举关联值

    每个枚举项可以关联一个值,这些值就是关联值

    enum Error {
        case error1(Int, Int, Int)
        case error2(Int, Int)
        case error3(Int)
        case error4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 枚举的内存大小
    let size = MemoryLayout.size              // 25
    let stride = MemoryLayout.stride          // 32
    let alignment = MemoryLayout.alignment    // 8
    
    • 1
    • 2
    • 3

    只需要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    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    我只分析了第一个变量的赋值:Error.error1(1, 2, 3)在内存中是1,2,3,0 ,其他的类似。

    我们也可以直接查看下Error.error1(1, 2, 3)内存的值:

    (lldb) register read sp
          sp = 0x000000016fdfdae0
    
    • 1
    • 2
    (lldb) x/4gx 0x000000016fdfdae0
    0x16fdfdae0: 0x0000000000000001 0x0000000000000002
    0x16fdfdaf0: 0x0000000000000003 0x00000001000d8000
    
    • 1
    • 2
    • 3

    内存中确实也是存储的 1 2 3 0 (注意:0x00000001000d8000 只需要看最后一个字节,因为数据只存了一个字节,其他的数据是脏数据, 所以只占用了25个字节)

    
    (lldb) x/4gx 0x000000016fdfdae0
    0x16fdfdae0: 0x0000000000000001 0x0000000000000002
    0x16fdfdaf0: 0x0000000000000003 0x00000001000d8000
    
    
    (lldb) x/4gx 0x000000016fdfdae0
    0x16fdfdae0: 0x0000000000000004 0x0000000000000005
    0x16fdfdaf0: 0x0000000000000000 0x00000001000d8001
    
    
    (lldb) x/4gx 0x000000016fdfdae0
    0x16fdfdae0: 0x0000000000000006 0x0000000000000000
    0x16fdfdaf0: 0x0000000000000000 0x00000001000d8002
    
    
    (lldb) x/4gx 0x000000016fdfdae0
    0x16fdfdae0: 0x0000000000000000 0x0000000000000000
    0x16fdfdaf0: 0x0000000000000000 0x00000001000d8003
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    枚举的方法

    Swift的枚举可以定义方法,我们看到枚举变量的内存和方法是不相关的,那枚举变量是如何和方法关联起来的呢?

    enum Direction: Int {
        case North
        case South
        case East = 9
        case West
        // 定义方法
        func dirMethod() -> Int {
            return 1
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们来看看方法调用的实现:

    func test() {
        let d = Direction(rawValue: 9)
        d?.dirMethod()
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

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

    总结
    • 枚举变量只占一个字节,如果超过255种情况不应该使用枚举
    • 枚举的内存中存储的值依次是 0,1,… n-1 整数(n为枚举成员的数量)
    • 如果枚举变量为nil, 则内存中存储的值是n(n为枚举成员的数量)
    • 如果枚举的原始值,则编译器会自动生成计算属性rawValue和初始化方法init?(rawValue: T) -> ()
    • 枚举的关联值放在内存的前面,和枚举类型值放在一起存储(关联值的存储长度由最长的那个枚举成员决定)
    • 枚举的方法调用会将枚举变量作为参数传入,实现枚举和方法的关联
  • 相关阅读:
    Visual Paradigm 里什么是复合结构图?
    【Unity3D赛车游戏优化篇】新【八】汽车实现镜头的流畅跟随,以及不同角度的切换
    Android 如何在Service中使用ViewModel
    南王科技IPO过会:年营收12亿 华莱士关联方持有近28%股权
    计算机毕业设计python基于django的学生考试成绩数据分析与可视化系统
    强大的UI组件集Telerik R3 2022支持.NET 7、全新的主题等
    在 Python 中打印二叉树
    PCL 点云镜像变换
    IPKISS Tutorials 3------绘制矩形版图
    Docker:使用 JupyterLab 进行数据科学
  • 原文地址:https://blog.csdn.net/lcl130/article/details/126503253