关联枚举的内存布局(二)

674 阅读6分钟

在上一篇关联枚举的内存布局(一)我们分析了关联类型为普通类型枚举的内存布局,这一篇我们分析一下关联类型为枚举类型的内存布局,先看一个小例子

enum A {
    enum B {
        case b1
        case b2
        case b3
    }
    case a1(a:B)
    case a2(a:B)
    case a3(a:B)
}
var a = A.a2(a: .b2, b: .b2)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)

按照我们上一篇文章的分析枚举A应该占用内存大小为1+1+1=3字节,但是结果却是2字节,那么我们看一下变量a的内存布局

通过二进制来分析一下

看情况苹果对关联值是枚举类型的情况又进一步做了优化,已经不是以字节为单位来确定是否可以存储case,而是以二进制位为单位了。

一个字节可以存储256个值,枚举A的3个case占用了2个二进制位,还剩6个二进制位最多可以存储64个值,我们验证一下B中有64个case的情况,这个时候应该还是1字节,不需要+1

enum A {
    enum B {
        case b1
        case b2
        case b3
        case b4
        ...假装有64case
        case b64
    }
    case a1(a:B)
    case a2(a:B)
    case a3(a:B)
}
var a = A.a2(a: .b64)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)
print("end")

结果符合预期,看一眼a的内存布局

再验证一下枚举B中65个case的情况,根据分析应该是+1的也就是2个字节,且分别为 0x41、0x01

enum A {
    enum B {
        case b1
        case b2
        case b3
        case b4
        ...假装有65case
        case b64
        case b65
    }
    case a1(a:B)
    case a2(a:B)
    case a3(a:B)
}
var a = A.a2(a: .b65)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)
print("end")

字节数+1了,符合预期,看一眼a的内存布局

这一步不符合预期,也就是苹果并没有以字节为单位拿来存储A中的case,通过二进制来分析一下

继续验证

enum A {
    enum B {
        case b1
        case b2
        case b3
        case b4
        ...假装有65case
        case b64
        case b65
    }
    case a1(a:B)
    case a2(a:B)
    case a3(a:B)
}
var a = A.a3(a: .b65)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)
print("end")

这个时候取a = A.a3的值,那么a的内存布局应该是

验证一下

符合预期!!

以上我们分析了枚举A中的case只有一个参数的情况,那么如果有多个枚举B类型参数的情况呢??

enum A {
    enum B {
        case b1
        case b2
        case b3
        case b4
        ...假装有65case
        case b64
        case b65
    }
    case a1(a:B,b:B)
    case a2(a:B,b:B)
    case a3(a:B,b:B)
}
var a = A.a2(a: .b65,b: .b65)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)
print("end")

这时A.a3的参数a占用了7个二进制位,参数b同样占用了7个二进制位,那么这两个字节分别可以提供1个二进制位给A拿来存储case

如果两个参数不是同一个类型呢?

enum A {
    enum B {
        case b1
        ...假装有65case
        case b65
    }
    
    enum C {
        case c1
        case c2
        case c3
        case c4
        case c5
        case c6
        case c7
        case c8
    }
    case a1(a:B,b:C)
    case a2(a:B,b:C)
    case a3(a:B,b:C)
    case a4(a:B,b:C)
    case a5(a:B,b:C)
    case a6(a:B,b:C)
    case a7(a:B,b:C)
    case a8(a:B,b:C)
}
var a = A.a8(a: .b65,b: .c7)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)
print("end")

这种情况比较特殊,第二个参数都是枚举C类型的,占用3个二进制位,如果是以下这种情况呢

enum A {
    enum B {
        case b1
        ...假装有65case
        case b65
    }
    
    enum C {
        case c1
        case c2
        case c3
        case c4
        case c5
        case c6
        case c7
        case c8
    }
    case a1(a:B,b:C)
    case a2(a:B,b:B) //这里第二个参数改为枚举B类型
    case a3(a:B,b:C)
    case a4(a:B,b:C)
    case a5(a:B,b:C)
    case a6(a:B,b:C)
    case a7(a:B,b:C)
    case a8(a:B,b:C)
}
var a = A.a8(a: .b65,b: .c7)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)
print("end")

enum A {
    enum B {
        case b1
        ...假装有65case
        case b65
    }
    
    enum C {
        case c1
        case c2
        case c3
        case c4
        case c5
        case c6
        case c7
        case c8
    }
    case a1(a:B,b:C)
    case a2(a:B,b:Int8)
    case a3(a:B,b:C)
    case a4(a:B,b:C)
    case a5(a:B,b:C)
    case a6(a:B,b:C)
    case a7(a:B,b:C)
    case a8(a:B,b:C)
}
var a = A.a8(a: .b65,b: .c7)
print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)
print("end")

总结:

计算枚举中case大小的规则:前面n-1个参数所占二进制位的和要为第n个参数所占用二进制位或64位的最小正整数倍

enum A{
    
    enum B {
        case b1
        case b2
        case b3
        case b4
    }
     case a1(t1:Int)
     case a2(t1:Int8,t2:B,t3:Int)
     case a3(t1:Int8,t2:Int,t3:B,t4:Int32)
     case a4(t1:Int8,t2:Int,t3:Int16,t4:Int,t5:B,t6:String)
}

print(MemoryLayout<A>.size)
print(MemoryLayout<A>.stride)

a1为64位不需要对齐 64/8=8字节

a2占用8+2+64,8/2=4不需要对齐,10/64无法整除则变为64+64=128位 128/8=16字节

a3占用8+64+2+32 8/64无法整除变为64+64+2+32,128/2可整除不需要对齐,130/32无法整除变为64+64+64+32=224位 224/8=28字节

a4占用8+64+16+64+2+128 8/64无法整除变为64+64+16+64+2+128,128/16可整除不需要对齐,144/64无法整除变为64+64+64+64+2+128, 258/64无法整除变为64+64+64+64+64+128=448位,虽然有String类型存在,但是这里不以128位对齐,仍然以64位对齐 448/8=56字节

如果不能整除8要进位

我们将实际内容存储区域成为真实存储位,将由于内存对齐被多分配出来的存储空间暂且称为扩充位(这是一个临时概念,帮助理解内容),将所有case的扩充位拿出来取交集,如果结果不为空,那么系统就可以确定当前给你分配的内存里边肯定有你用不到的区域,这个区域就可以用来存储case

系统将每一个字节中你确定用不到的二进制位拿出来存储枚举中的case,如果这些位不够用再+1个字节,当关联类型不是枚举类型时由于这些类型都是以字节为单位划分的,所以用新开辟的一个字节存储case