用过Swift enum的同学肯定已经被他的强大所吸引, 今天我们不专门讲解Swift enum的高级用法, 而是对 enum在内存中存储的大小进行一次深入探究
0. 准备工作
-
- MemoryLayout
MemoryLayout可以帮我们计算类型在内存中所占用的空间
MemoryLayout.size // 占用的内存大小 MemoryLayout.stride // 分配的内存大小 MemoryLayout.alignment // 内存的对齐大小
-
- withUnsafePointer()
withUnsafePointer()方法可以获取swift中对象的指针地址
-
- 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
}
我们可以看到, 对于一个普普通通的 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
}
可以看到与普通的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
}
可以看到, 带有关联值的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
}
可以看到这次带有关联值的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可以声明方法和计算属性