Swift枚举底层研究

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

编译器自动生成了一个存储属性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初始化方法

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()
}
method

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

总结

  • 枚举变量只占一个字节,如果超过255种情况不应该使用枚举
  • 枚举的内存中存储的值依次是 0,1,... n-1 整数(n为枚举成员的数量)
  • 如果枚举变量为nil, 则内存中存储的值是n(n为枚举成员的数量)
  • 如果枚举的原始值,则编译器会自动生成计算属性rawValue和初始化方法init?(rawValue: T) -> ()
  • 枚举的关联值放在内存的前面,和枚举类型值放在一起存储(关联值的存储长度由最长的那个枚举成员决定)
  • 枚举的方法调用会将枚举变量作为参数传入,实现枚举和方法的关联
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,966评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,170评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,909评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,959评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,851评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,583评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,956评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,590评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,878评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,892评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,719评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,501评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,957评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,124评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,440评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,003评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,211评论 2 339

推荐阅读更多精彩内容