Swift Enum-内存初探

5,184 阅读4分钟
  1. Swift Enum-细说枚举
  2. Swift Enum-内存初探

用过Swift enum的同学肯定已经被他的强大所吸引, 今天我们不专门讲解Swift enum的高级用法, 而是对 enum在内存中存储的大小进行一次深入探究

0. 准备工作

    1. MemoryLayout

    MemoryLayout可以帮我们计算类型在内存中所占用的空间

    MemoryLayout.size // 占用的内存大小
    MemoryLayout.stride // 分配的内存大小
    MemoryLayout.alignment // 内存的对齐大小
    
    1. withUnsafePointer()

    withUnsafePointer()方法可以获取swift中对象的指针地址

    1. Swift编译器中间码SIL

    SIL是 Swift编译器产生的中间产物, 通过SIL我们可以简单的分析代码的执行过程

    推荐一篇大佬的讲解: juejin.cn/post/684490…

1. 探究普通的enum

enum Color {
    case red
    case yellow
    case blue
}

func test() {
    var c1 = Color.red
    print(MemoryLayout.size(ofValue: c1)) // 1
    
    var c2 = Color.yellow
    print(MemoryLayout.size(ofValue: c2)) // 1
    
    var c3 = Color.blue
    print(MemoryLayout.size(ofValue: c3)) // 1
}

1.png 我们可以看到, 对于一个普普通通的 enum, 他在内存中只占用一个字节, 甚至在内存中他存储的仅仅是0, 1, 2.....

unsigned numTagBytes = (numTags <=    1 ? 0 :
                          numTags <   256 ? 1 :
                          numTags < 65536 ? 2 : 4);

在enum的源码中我们找到了一些线索, 一个枚举所占的字节数 numTagBytes 跟case的个数有关, 如果你问为什么只有 0,1,2,4 ? 我想如果一个枚举的case 6.5w都不够的话, 我会送你一个 "???"

对于普通的枚举, 我们只需要一个字节就可以记录枚举具体表示哪一个值.

2. 探究带有原始值的enum

enum Color2: String {
    case red = "redColor"
    case yellow = "yellowColor"
    case blue = "blueColor"
}

func test() {      
    var c1 = Color2.red
    print(MemoryLayout.size(ofValue: c1)) // 1

    var c2 = Color2.yellow
    print(MemoryLayout.size(ofValue: c2)) // 1

    var c3 = Color2.blue
    print(MemoryLayout.size(ofValue: c3)) // 1
}

2.png

可以看到与普通的enum一样, 带有原始值的enum同样只占一个字节的空间, 并且存储的同样是0,1,2 也就是标志位, 那么我们是如何拿到enum的rawValue呢? 这里就需要借助sil了

/**
......
// function_ref Color2.rawValue.getter
%5 = function_ref @$s14ViewController6Color2O8rawValueSSvg : $@convention(method) (Color2) -> @owned String // user: %6
......

// Color2.rawValue.getter
sil hidden @$s14ViewController6Color2O8rawValueSSvg : $@convention(method) (Color2) -> @owned String {
    // %0 "self"                                      // users: %2, %1
    bb0(%0 : $Color2):
    switch_enum %0 : $Color2, case #Color2.red!enumelt: bb1, case #Color2.yellow!enumelt: bb2, case #Color2.blue!enumelt: bb3 // id: %2

    bb1:                                              // Preds: bb0
    %3 = string_literal utf8 "redColor"             // user: %8
    ......
    br bb4(%8 : $String)                            // id: %9

    bb2:                                              // Preds: bb0
    %10 = string_literal utf8 "yellowColor"         // user: %15
    ......
    br bb4(%15 : $String)                           // id: %16

    bb3:                                              // Preds: bb0
    %17 = string_literal utf8 "blueColor"           // user: %22
    ......
    br bb4(%22 : $String)                           // id: %23

    // %24                                            // user: %25
    bb4(%24 : $String):                               // Preds: bb3 bb2 bb1
    return %24 : $String                            // id: %25
} // end sil function '$s14ViewController6Color2O8rawValueSSvg'
*/

从sil中, 我们可以看到 function_ref Color2.rawValue.getter 这样一句代码, 也就是说 rawValue其实是一个getter方法, 在具体的方法中, 使用switch_enum对枚举进行了匹配, 并且执行了 bb1, bb2, bb3中的某一个, 而 bbn 则是直接返回了一个字符串而已.

所以, 带有原始值的enum也是只占用一个字节的空间, 并且存储自己的标志位

3. 探究带有关联值的enum

enum Color3 {
    case red
    case green(val: Bool)
    case yellow(val1: Int, val2: Int32)
    case blue(val:Int)
}

func test() {
    
    var c1 = Color3.red
    print(MemoryLayout.size(ofValue: c1)) // 13

    var c2 = Color3.green(val: true)
    print(MemoryLayout.size(ofValue: c2)) // 13

    var c3 = Color3.yellow(val1: 9, val2: 22) 
    print(MemoryLayout.size(ofValue: c3)) // 13

    var c4 = Color3.blue(val: 14)
    print(MemoryLayout.size(ofValue: c4)) // 13
}

3.png

可以看到, 带有关联值的enum, 占用了13个字节. 在图3中, 可以看到前12个字节存储了enum关联的具体值(红色框), 最后一个字节存储的是enum的标志位. 这样的存储结构, 我们就可以通过标志位判断具体是哪一个case, 通过前12个自己拿到关联值的具体值.

可以看出: 例子中关联值的枚举占用的内存大小为: 8(存储Int) + 4(存储Int32) + 1(存储标志位),

那么我们是否可以做出总结: 关联值的枚举占用的内存大小 = (存储关联值最大case)需要的大小 + 1 呢?

4. 探究带有关联值的enum(特殊)

enum Color4 {
    case red
    case green(val: Bool)
    case blue(val: Bool)
}

func test() {
        
    var c1 = Color4.red
    print(MemoryLayout.size(ofValue: c1)) // 1
    
    var c2 = Color4.green(val: true)
    print(MemoryLayout.size(ofValue: c2)) // 1

    var c3 = Color4.blue(val: true)
    print(MemoryLayout.size(ofValue: c3)) // 1
}

4.png 可以看到这次带有关联值的enum竟然也只占用一个字节. 但是仔细思考一下, 好像确实一个字节就够了. 如图4中, 红框的第一位(0,4,8) 表示每个case的标志位, 红框的第二位(0,1,1)表示关联Bool的值.

所以, 带有关联值的枚举具体占用空间还是需要具体情况具体分析

5. 探究enum的方法和计算属性

enum Color5 {
    case red
    case yellow
    case blue
  
    var intValue: Int {
        return 20
    }
    func desc() -> String {
        return "desc"
    }
    static func staticDesc() -> String {
        return "staticDesc"
    }
}

func test() {
    var c = Color5.red
    _ = c.desc()       // "desc"
    _ = Color5.staticDesc()  // "staticDesc"
    _ = c.intValue
}

/**   Color5.desc()
// function_ref Color5.desc()
%5 = function_ref @$s14ViewController6Color4O4descSSyF : $@convention(method) (Color4) -> @owned String // user: %6

// Color5.desc()
sil hidden @$s14ViewController6Color4O4descSSyF : $@convention(method) (Color4) -> @owned String {
    // %0 "self"                                      // user: %1
    bb0(%0 : $Color5):
    %2 = string_literal utf8 "desc"                 // user: %7
    .......
    return %7 : $String                             // id: %8
} // end sil function '$s14ViewController6Color4O4descSSyF'
*/

/**  Color5.staticDesc()
%8 = metatype $@thin Color5.Type  // user: %10
// function_ref static Color5.staticDesc()
%9 = function_ref @$s14ViewController6Color4O10staticDescSSyFZ : $@convention(method) (@thin Color5.Type) -> @owned String // user: %10

// static Color5.staticDesc()
sil hidden @$s14ViewController6Color4O10staticDescSSyFZ : $@convention(method) (@thin Color5.Type) -> @owned String {
    // %0 "self"                                      // user: %1
    bb0(%0 : $@thin Color5.Type):
    %2 = string_literal utf8 "staticDesc"           // user: %7
    .........
    return %7 : $String                             // id: %8
} // end sil function '$s14ViewController6Color4O10staticDescSSyFZ'
*/

/** Color5.intValue
// function_ref Color5.intValue.getter
%12 = function_ref @$s14ViewController6Color4O8intValueSivg : $@convention(method) (Color5) -> Int // user: %13

// Color5.intValue.getter
sil hidden @$s14ViewController6Color4O8intValueSivg : $@convention(method) (Color4) -> Int {
    // %0 "self"                                      // user: %1
    bb0(%0 : $Color5):
    %2 = integer_literal $Builtin.Int64, 20         // user: %3
    ......
    return %3 : $Int                                // id: %4
} // end sil function '$s14ViewController6Color4O8intValueSivg'
*/

根据sil可以看到, 不管是普通方法还是static方法还是计算属性, 都是简单的方法调用, 这也就是为什么Swift 中enum可以声明方法和计算属性