美文网首页
iOS-Swift-枚举变量的内存布局

iOS-Swift-枚举变量的内存布局

作者: Imkata | 来源:发表于2020-01-06 14:15 被阅读0次

    枚举章节讲了下枚举,本文就详细分析枚举变量的内存布局。

    创建命令行项目,执行如下代码:

    var a = 10
    print(a) //打断点
    

    点击View Memory of "a",如下:

    View Memory of "a"

    可以发现变量a内存结构如下图:

    a内存结构

    A就是16进制的10,这种方式可以查看a的内存结构。

    但是使用这种方式查看枚举变量的内存就是空,苹果不给我们查看枚举变量的内存结构。

    一. 窥探枚举内存

    1. 简单枚举

    在命令行项目运行如下代码:

    enum TestEnum {
        case test1, test2, test3
    }
    
    var t1 = TestEnum.test1
    var t2 = TestEnum.test2
    var t3 = TestEnum.test3
    print("123")
    

    枚举章节的学习我们知道了上面的枚举变量t1、t2、t3都是占用1字节,下面就通过窥探内存验证一下的确占用1字节。
    但是如果使用文章刚开始的那种方式查看枚举变量的内存就是空,苹果不给我们查看枚举变量的内存结构,下面我们使用Mems.swift
    打印枚举变量的内存地址,然后通过地址窥探内存布局。

    运行代码:

    enum TestEnum {
        case test1, test2, test3
    }
    
    var t1 = TestEnum.test1
    var t2 = TestEnum.test2
    var t3 = TestEnum.test3
    
    print(Mems.ptr(ofVal: &t1))
    print(Mems.ptr(ofVal: &t2))
    print(Mems.ptr(ofVal: &t3))
    
    print("123") //打断点
    

    打印地址:

    0x0000000100007928
    0x0000000100007929
    0x000000010000792a
    

    点击Debug -> Debug Workflow -> View Memory,输入内存地址:0x0000000100007928

    0x0000000100007928

    可以发现前三个字节分别存储的是00 01 02,验证了上面的枚举的确占用一个字节,并且枚举变量在内存中直接存储的是0 1 2。

    如果给上面枚举添加原始值:

    enum TestEnum :Int {
        case test1 = 1, test2 = 2, test3 = 3
    }
    
    var t1 = TestEnum.test1
    var t2 = TestEnum.test2
    var t3 = TestEnum.test3
    
    print(Mems.ptr(ofVal: &t1))
    print(Mems.ptr(ofVal: &t2))
    print(Mems.ptr(ofVal: &t3))
    
    print("123") //打断点
    

    同样的方式也可以验证,上面枚举变量占用的也是一个字节,并且枚举变量在内存中直接存储的也是0 1 2。

    如果是添加关联值的枚举呢?

    2. 带关联值的枚举

    enum TestEnum {
        case test1(Int, Int, Int)
        case test2(Int, Int)
        case test3(Int)
        case test4(Bool)
        case test5
    }
    
    var e = TestEnum.test1(1, 2, 3)
    
    MemoryLayout.size(ofValue: e) 
    MemoryLayout.stride(ofValue: e) 
    MemoryLayout.alignment(ofValue: e)
    

    打印:

    25
    32
    8
    

    可以发现e实际占用25字节,系统分配了32字节,内存对齐为8。

    下面通过查看内存看一下为什么是这样的?运行代码:

    var e = TestEnum.test1(1, 2, 3)
    print(Mems.ptr(ofVal: &e))
    

    同样的方式,打印内存地址,查看内存:

     小端:高高低低
     01 00 00 00 00 00 00 00
     02 00 00 00 00 00 00 00
     03 00 00 00 00 00 00 00
     00
     00 00 00 00 00 00 00
    

    补充:
    CPU读取内存的方式分为大、小端模式,现在的CPU一般是小端模式,就是读数据的时候内存地址比较高的放在数据的高字节,内存地址比较低的放在数据的低字节,就是高高低低,所以 01 00 00 00 00 00 00 00读出来之后就是:0x00 00 00 00 00 00 00 01

    可以看出前8个字节存放01,后8个字节存放02,再后面8个字节存放03,最后一个字节存放00。

    当:e = .test2(4, 5)

     04 00 00 00 00 00 00 00
     05 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     01
     00 00 00 00 00 00 00
    

    当:e = .test3(6)

     06 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     02
     00 00 00 00 00 00 00
    

    当:e = .test4(true)

     01 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     03
     00 00 00 00 00 00 00
    

    当:e = .test5

     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     04
     00 00 00 00 00 00 00
    

    可能你已经看出规律了,上面枚举在内存中的存储方式是前24个字节分别存储枚举的关联值,后1个字节存储枚举的成员值。

    总结:
    1个字节存储成员值。
    N个字节存储关联值(N取占用内存最大的关联值),任何一个case的关联值都共用这N个字节。

    如果枚举中只有一个case呢?

    enum TestEnum {
        case test
    }
    
    var e = TestEnum.test
    
    MemoryLayout.size(ofValue: e) 
    MemoryLayout.stride(ofValue: e) 
    MemoryLayout.alignment(ofValue: e)
    

    打印:

    0
    1
    1
    

    可以发现,e实际占用0字节,系统分配了1字节,内存对齐为1。
    这个很好理解,因为就一个成员,编译器一看就知道肯定是test,根本不用管是哪个case,所以就不需要内存。

    如果只有一个case并且有一个关联值呢?

    enum TestEnum {
        case test(Int)
    }
    
    var e = TestEnum.test(10)
    
    MemoryLayout.size(ofValue: e) 
    MemoryLayout.stride(ofValue: e) 
    MemoryLayout.alignment(ofValue: e)
    

    打印:

    8
    8
    8
    

    这个也很好理解,因为只有一个case,所以根本不需要1字节来存储成员值,只需要用8个字节来存储关联值就好了。

    作业:进一步观察下面枚举的内存布局

    作业.png

    二. Mems.swift的使用

    上面我们都是打印地址,然后根据地址查看内存的,这样有点麻烦,其实Mems.swift也可以直接打印内存布局的

    比如打印上面的test2(4, 5):

    var e = TestEnum.test2(4, 5)
    print(Mems.memStr(ofVal: &e))
    

    打印:

    0x00 00 00 00 00 00 00 04
    0x00 00 00 00 00 00 00 05
    0x00 00 00 00 00 00 00 00
    0x00 00 00 00 00 00 00 01
    

    默认是按照对齐方式来打印的,上面对齐是8,所以每8字节打印一次,如果想按其他方式打印,可传入参数,如下就是每2个字节打印一次:

    print(Mems.memStr(ofVal: &e, alignment: .two))
    

    三. 它们的switch语句底层又是如何实现的?

    func testEnum() {
        enum TestEnum {
            case test1(Int, Int, Int)
            case test2(Int, Int)
            case test3(Int)
            case test4(Bool)
            case test5
        }
        
        var e = TestEnum.test1(10, 20, 30)  //打此处断点
        print(Mems.ptr(ofVal: &e))
    
        switch e {
        case let .test1(v1, v2, v3):
            print("test1", v1, v2, v3)
            
        case let .test2(v1, v2):
            print("test2", v1, v2)
            
        case let .test3(v1):
            print("test3", v1)
            
        case let .test4(v1):
            print("test4", v1)
            
        case .test5:
            print("test5")
        }
    }
    

    上面switch底层是怎么实现的呢?
    当 switch e 的时候会先取出e的成员值
    如果e的成员值是0,那么就匹配test1,然后就把枚举内存中的前8个字节(也就是10)赋值给v1,后8个字节(也就是20)赋值给v2,再后8个字节(也就是30)赋值给v3。
    如果e的成员值是1,那么就匹配test2,然后把枚举内存中的前8个字节赋值给v1,后8个字节赋值给v2。
    ......
    如果e的成员值是4,那么就匹配test5,直接打印。

    结论我们知道了,下面通过查看汇编验证,验证之前不熟悉汇编的一定要先看一下汇编

    执行上面代码,打开显示汇编,汇编代码如下:

    ->  0x100001c1f <+63>:   movq   $0xa, 0x5cde(%rip) //第一句
    0x100001c2a <+74>:   leaq   0x5cd7(%rip), %rax  //第二句
    0x100001c31 <+81>:   movq   $0x14, 0x5cd4(%rip)  //第三句
    0x100001c3c <+92>:   movq   $0x1e, 0x5cd1(%rip)  //第四句
    0x100001c47 <+103>:  movb   $0x0, 0x5cd2(%rip)  //第五句
    

    如果看过汇编,上面的代码会很容易理解,这里我们一句一句解释。

    第一句:将10赋值到内存地址:rip+0x5cde = 0x100001c2a + 0x5cde = 0x100007908,占用8字节
    第二句:将0x5cd7(%rip)地址赋值给rax寄存器,0x5cd7(%rip) = 0x5cd7 + 0x100001c31 = 0x100007908
    第三句:将20赋值到内存地址:rip+0x5cd4 = 0x100001c3c + 0x5cd4 = 0x100007910,占用8字节
    第四句:将30赋值到内存地址:rip+0x5cd1 = 0x100001c47 + 0x5cd1 = 0x100007918,占用8字节
    第五句:将0存储到内存地址:rip+0x5cd2 = 0x100001c4e + 0x5cd2 = 0x100007920,只占1个字节

    总结:

    1. 这5句汇编就是var e = TestEnum.test1(10, 20, 30),只是简单的赋值,没函数调用
    2. 由于枚举的第一个成员的内存地址就是整个枚举变量的地址,所以上面的0x100007908就是枚举变量e的内存地址
    3. 第一、三、四句分别是将10、20、30存到内存中(也就是将枚举的关联值存入内存)
    4. 第五句是将0存到内存中(也就是将枚举的成员值存入内存)

    补充:

    0x100001c31 <+81>:   movq   $0x14, 0x5cd4(%rip)  //第三句
    0x100001c3c <+92>:   movq   $0x1e, 0x5cd1(%rip)  //第四句
    

    上面汇编,0x100001c3c代表这条汇编指令的内存地址,<+92>代表从函数最开始的0字节到这条指令中间有多少字节,通过这个数字我们可以算出上条指令占用多少字节,比如92 - 81 = 11,所以第三句指令占用11字节。

    相关文章

      网友评论

          本文标题:iOS-Swift-枚举变量的内存布局

          本文链接:https://www.haomeiwen.com/subject/zfdtactx.html