Swift底层探索(九)Swift枚举的底层源码探索

1,002 阅读7分钟

我正在参加「掘金·启航计划」

主要内容:

  1. 枚举的底层结构体认识
  2. rawValue的实现原理
  3. init的实现原理
  4. 模式匹配过程
  5. 内存大小分析
  6. Swift和OC混编

1. 枚举的底层结构体认识

1.1 原始值

代码:

/*
 1、原始值
 */
enum Week: String{
    case MON = "MON"
    case TUE = "TUE"
    case WED = "WED"
    case THU = "THU"
    case FRI = "FRI"
    case SAT = "SAT"
    case SUN = "SUN"
}

SIL:

//只有原始值
//自动生成init?初始化方法,已经rawValue计算属性
enum Week : String {
  case MON
  case TUE
  case WED
  case THU
  case FRI
  case SAT
  case SUN
  //参数是rawValue
  init?(rawValue: String)
  //RawValue类型也就是String
  typealias RawValue = String
  //rawValue只读计算属性
  var rawValue: String { get }
}

说明:

1.2 关联值

代码:

SIL:

16621874236476.jpg

说明:

  • 当使用了关联值后,就没有RawValue了,主要是因为case可以用一组值来表示,而rawValue是单个的值
  • 注意只要枚举中存在一个关联值,那么这个枚举就不存在rawValue了。(后面好好分析一下)

2. rawValue的实现原理

代码:

/*
 3、rawValue的实现原理
*/
enum Week: String{
    case MON = "MON"
    case TUE = "TUE"
    case WED = "WED"
    case THU = "THU"
    case FRI = "FRI"
    case SAT = "SAT"
    case SUN = "SUN"
}

let mon = Week.MON.rawValue

SIL的main方法:

16621901631769.jpg

说明:

  1. 拿到枚举
  2. 获取到getter方法
  3. apply调用方法,并且传入枚举
  4. 赋值给全局变量
  5. 因此这里最重要的就是getter方法传入枚举获取到rawValue值

SIL的getter方法:

16621905509970.jpg

说明:

  1. 通过传入的枚举通过匹配枚举项找到对应的分支
  2. 在不同的分支上构建对应的字符串
  3. 跳转到b8返回这个字符串,而这个字符串就是rawValue拿到的值
  4. 构建字符串本质就是到底层获取到字符串

在底层存储的字符串:

16621870815169.jpg

3. init的实现原理

3.1 调用时机

代码:

/*
 4、init方法
 */
enum Week: String{
    case MON = "MON"
    case TUE = "TUE"
    case WED = "WED"
    case THU = "THU"
    case FRI = "FRI"
    case SAT = "SAT"
    case SUN = "SUN"
}
print(Week.MON.rawValue)

let w = Week.MON.rawValue

Week.init(rawValue: "MON")

print("end")

定义一个符号断点

16621922256890.jpg

说明:

  • 可以看到只有通过Init方法进行调用时才会执行init方法
  • 直接获取ravwValue,是不会执行init流程的

3.2 底层分析

代码:

16621923354691.jpg

SIL的init方法:

16621932234780.jpg

说明:

  1. 在init方法中是将所有enum的字符串从Mach-O文件中取出,依次放入数组中
  2. 放完后,然后调用_findStringSwitchCase方法进行匹配
  3. 1、先创建一个数组
  4. 2、将第一个值存储到数组中
  5. 3、计算得到第二个值的地址,将第二个值存储到数组中
  6. 4、依次执行,把所有元素放到数组中

_findStringSwitchCase: 通过init方法来创建枚举对象时,需要判断枚举中是否存在该元素

16621934851297.jpg

CDE8665810247A531254B25763A7A8A5.jpg

说明:

  • 1、遍历数组,如果匹配则返回对应的index
  • 2、如果不匹配,则返回-1

匹配之后的判断:

16621940655027.jpg

16621940200693.jpg

说明:

  • 如果没有匹配成功,则构建一个.none类型的Optional,表示nil
  • 如果匹配成功,则构建一个.some类型的Optional,表示有值

4. 模式匹配过程

4.1 原始值

代码:

/*
 5、模式匹配,原始值
 */
/*
enum Week: String{
    case MON
    case TUE
    case WED
    case THU
    case FRI
    case SAT
    case SUN
}

var current: Week?
switch current {
    case .MON:print(Week.MON.rawValue)
    case .TUE:print(Week.MON.rawValue)
    case .WED:print(Week.MON.rawValue)
    default:print("unknow day")
}

SIL:

16621947061222.jpg

说明:

  • 其内部是将nil放入current全局变量,然后匹配case,做对应的代码跳转

4.2 关联值

代码:

/*
 6、模式匹配,关联值
 */
enum Shape{
    case circle(radius: Double)
    case rectangle(width: Int, height: Int)
}

let shape = Shape.circle(radius: 10)
switch shape{
    case .circle(let radius):
        print("circle radius: \(radius)")
    case .rectangle(let width, var height):
        height += 1
        print("rectangle width: \(width) height: \(height)")
}

SIL:

16621952873170.jpg

说明:

  1. 首先构建一个关联值的元组
  2. 根据当前case枚举值,匹配对应的case,并跳转
  3. 取出元组中的值,将其赋值给匹配case中的参数

5. 内存大小分析

5.1 原始值

代码:

/*
 7、内存大小
 */
//只有一个成员
enum NoMean1{
    case a
}
print(MemoryLayout<NoMean1>.size)
print(MemoryLayout<NoMean1>.stride)

//多个成员
enum NoMean2{
    case a
    case b
}
print(MemoryLayout<NoMean2>.size)
print(MemoryLayout<NoMean2>.stride)

结果:

16621954144774.jpg

说明:

  • 一个成员,不占用内存
  • 多个成员时,会占用一个字节大小的内存

LLDB查看:

enum NoMean{
    case a
    case b
    case c
    case d
}

var tmp = NoMean.a
var tmp1 = NoMean.b
var tmp2 = NoMean.c
var tmp3 = NoMean.d

16621956825878.jpg

说明:

  • 在枚举内存中存储的是序号,用来标记枚举成员(从0开始,总共4个序号)
  • case是UInt8,即1字节(8位),最大可以存储255
  • 如果超过了255,会自动从UInt8 -> UInt16 -> UInt32 -> UInt64 ...(再看下教案)

总结:

  1. 如果enum中有原始值,即rawValue,其大小取决于case的多少,如果没有超过UInt8即255,则就是1字节存储case
  2. 只有一个成员时,此时不需要使用序号标记,因此枚举内存没有存储任何值
  3. 如果有多个成员,就需要使用需要标记成员,枚举就会占用1个字节大小的内存

5.2 关联值

代码:

/*
 8、内存大小:关联值
 */
enum Shape{
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)

结果:

16621957912788.jpg

LLDB:

16621961218251.jpg

说明:

  1. num有关联值时,关联值的大小 取 对应枚举关联值 最大的
  2. circle中关联值大小是8,而rectangle中关联值大小是16,所以取16
  3. 但是因为有多个成员,所以还需要一个字节大小用来存储序号,所以就是17个
  4. 分配内存时遵循字节对齐原则,因此分配了24个字节的空间

5.3 递归枚举

使用关键字indirect就可以实现枚举的递归 ,可以修饰在enum前,也可以修饰在枚举内的成员前

5.3.1 使用:

详细的可以看另一篇博客

/*
 9、递归枚举
 */
//用枚举表示链表结构
//放在case前
enum List1<T>{
    case end
    //表示case使是引用来存储
    indirect case node(T, next: List1<T>)
}

//放在enum前
indirect enum List2<T>{
    case end
    case node(T, next: List2<T>)
}

5.3.2 查看大小:

代码:

16621963235301.jpg

说明:

  • 不管是String类型还是Int类型,此时都是8个字节,而且占用内存大小就是8个字节
  • 此时枚举包含两个成员,但是end是原始值,所以内存大小取决于node的成员的大小

5.3.3 LLDB查看内存结构

1、先查看node

代码:

enum List<T>{
    case end
    indirect case node(T, next: List<T>)
}

var node = List<Int>.node(10, next: List<Int>.end)

print(MemoryLayout.size(ofValue: node))
print(MemoryLayout.stride(ofValue: node))

LLDB:

16621966233446.jpg

说明:

  • 如果枚举被indirect修饰,编译器就不再计算这个枚举的大小
  • 在堆中开辟了一个空间,将这个地址放到枚举中
  • 因此递归的时候,是无法确定实际大小的,只能通过运行中递归的判断条件来决定

2、再查看end

代码:

LLDB:

16621970948227.jpg

说明:

  • 如果存储的是end,那么他就和普通的枚举一样
  • 只不过这里存储的值是的当前序号的2倍

6. Swift和OC混编

上面我们可以知道swift有很强大的功能,可以添加方法属性,还有关联值原始值,并且成员有多种类型,而OC的枚举只能是Int类型,局限性比较大,所以如果要进行混编,就需要进行一定的特殊考虑

6.1 OC调用Swift

代码:

<!--swift中定义-->
@objc enum Week: Int{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

<!--OC使用-->
- (void)test{
    Week mon = WeekMON;
}

说明:

  • 用@objc进行修饰,暴露给OC
  • 必须是Int类型

OC如何访问swift中String类型的enum:

@objc enum Week: Int{
    case MON, TUE, WED
    
    var val: String?{
        switch self {
        case .MON:
            return "MON"
        case .TUE:
            return "TUE"
        case .WED:
            return "WED"
        default:
            return nil
        }
    }
}

<!--OC中使用-->
Week mon = WeekMON;

<!--swift中使用-->
let Week = Week.MON.val

说明:

  • swift中的enum尽量声明成Int整型
  • 然后OC调用时,使用的是Int整型的
  • enum在声明一个变量/方法,用于返回固定的字符串,用于在swift中使用

6.2 Swift调用OC

6.3.1 方式一:typedef enum

OC代码:

Swift代码:

说明:

  1. OC的这种声明方式,会转换成Swift的结构体
  2. 之后就需要注意使用时的样式
  3. 并遵循了两个协议:Equatable 和 RawRepresentable

6.3.2 方式二:typedef NS_ENUM

OC代码:

Swift代码:

说明:

  • OC指定类型的方式,Swift也会指定类型

6.3.3 方式三:NS_ENUM

OC代码:

Swift代码:

说明:

  • 自动转换

7、总结