Swift进阶-值类型&引用类型&方法调度

5,948 阅读10分钟

Swift中,提到值类型我们通常会想到struct,而类是引用类型,那么结构体为什么是值类型,类为什么又是引用类型呢?本文将从结构体和类触发,来探究值类型和引用类型的区别

值类型

  • 下面从一个案例来分析值类型:

    func valueTest() {
        var age1: Int = 18
        var age2: Int = 20 
        var age3: Int = age1
        age3 = 26
    }
    valueTest()
    
    • 打印三个临时变量的内存地址结果如下:

    截屏2022-02-11 14.17.31.png

    • 在打印结果中可以看到:age1age3的地址不同,且age3赋值后他们值也不同,说明age3 = age1的过程相当于深拷贝,说明age就是值类型
    • 0x7开头的内存代表栈区,且栈区内存是连续的,关于内存方面可以参考 内存分区与布局

结构体

  • 下面来定义两个结构体:

    struct WSPerson {
        var age: Int = 18
    }
    
    struct WSTeacher {
        var age: Int
    }
    
    // 初始化
    var person = WSPerson()
    var teacher = WSTeacher(age: )
    
    • 两个结构体中的成员一个有值,但初始化方法就产生了不同,下面通过Sil文件查看源码

    截屏2021-12-14 16.13.38.png

    • Sil文件中WSPerson有两个init函数,其中一个对age进行了赋值,所以当成员变量有值时,可以不对它赋新值。而WSTeacher初始化init方法只有一个,所以两个结构体的初始化方法不一样。

下面对有初始值类型的struct进行分析

struct是值类型分析

  • 定义结构体如下:

    struct WSPerson {
        var age1: Int = 18
        var age2: Int = 22
    }
    
    var wushuang = WSPerson()
    var tony = wushuang
    tony.age1 = 19
    
    • 两个结构体对象相关打印结果如下:

      截屏2021-12-15 14.34.48.png

    • 通过打印发现二者的值与地址都不同,地址中存储的直接是成员的值

  • 再对tonywushuang进行Sil分析

    截屏2021-12-15 11.10.28.png

    • main函数中主要是先进行wushuang的创建,再拷贝一份给tony,下面再来看看wushuang创建的核心逻辑init

    截屏2021-12-15 11.29.40.png

    • 创建内存的代码中主要是在栈区开辟内存,以及对成员变量的处理,所以 结构体是值类型

总结:
1. 结构体开辟的内存在栈区
2. 结构体的赋值是深拷贝

引用类型

  • 先来看看class的几种初始化方式:

    class WSCat {
        var age: Int
        init(age: Int) {
            self.age = age
        }
    }
    class WSDog {
        var age: Int?
    }
    
    class WSTeacher {
        var age: Int = 18
    }
    
    var cat = WSCat(age: 2)
    var dog = WSDog()
    var teacher = WSTeacher()
    
    • 当类中的属性有值或者是可选类型时,可以不用重写init方法;当属性没有值时,必须要重写init方法

    • 接着打印teacher相关信息结果如下:

      截屏2021-12-15 15.55.27.png

    • 从打印内容可以看出teacher是指针,它指向的是类在堆区的首地址,从类里面可以读取到类的相关信息

class是引用类型分析

  • 创建一个对象teacher2,并将teacher赋值给它,打印相关信息结果如下:

    截屏2021-12-15 16.41.47.png

    • 虽然新对象的地址不同,但他们所指向的堆区内存一致,所以他们操作的是同一片内存空间,我们可以通过打印二者的age值来验证:

      截屏2021-12-15 16.50.35.png

    • 结果两个age的值相同,所以class对象的赋值是浅拷贝,进而得出class引用类型

值类型嵌套引用类型

  • 将代码改成值类型嵌套引用类型,代码如下:

    class WSDog {
        var dogAge: Int = 3
    }
    
    struct WSPerson {
        var age: Int = 18
        var dog = WSDog()
    }
    
    var person1 = WSPerson()
    var person2 = person1
    person2.dog.dogAge = 5
    
    • 打印两个对象结果如下:

      截屏2021-12-15 17.11.14.png

    • 虽然person是值类型,但里面的dog是引用类型,他们操作的是同一片内存,所以两个对象中的dog.dogAge值是一样的

Mutating & inout

  • 在定义结构体时,在结构体的方法中不允许修改实例变量,如下图:

    截屏2021-12-15 22.05.50.png

    • 将方法里的修改变量值修改下:
    struct WSPerson {
        var age: Int = 0
      
        func add(_ count: Int) {
            print(count)
        }
    }
    
    • 生成Sil文件并查看add方法:

    截屏2021-12-15 22.25.57.png

    • add方法中,有个let类型的self,也就是此时的结构体不可变,如果改变age,实质是改变结构体本身,所以在方法中修改成员变量的值会报错。
  • self用可变类型接收,结果不会报错:

    struct WSPerson {
        var age: Int = 0
      
        func add(_ count: Int) {
            var s = self
            s.age += count
        }
    }
    
    var person = WSPerson()
    person.add(3)
    print(person.age)
    
    • 打印结果如下:

    截屏2021-12-15 22.38.57.png

    • 由于结构体是值类型,所以此时的s是深拷贝,改变的值是s中的,与person对象无关,所以此时打印依旧是0
  • 将方法添上之前报错提示mutating,此时就可以修改实例变量的值:

    截屏2021-12-15 22.43.46.png

    • 生成Sil文件并查看add方法

    截屏2021-12-15 22.47.25.png

    • 观察发现方法添加mutating后,有以下变化:
        1. 参数中的WSPerson增加了inout修饰
        1. self访问的是地址
        1. selfvar可变类型
    • 所以值的修改直接修改的是person地址,所以可以修改成功
  • 上面出现的inout有什么作用我们不得而知,下面通过案例来分析下

    截屏2021-12-15 22.57.50.png

    • 由于参数都是let类型,所以不可以修改,此时可以加上inout对参数进行修饰:

      截屏2021-12-15 23.04.18.png

    • 参数添加intout后,则传入的参数就是地址,所以此时参数可以进行修改

方法调度

  • 在上面分析中我们知道结构体是值类型,那么它的方法在哪呢?下面我们将对结构体和类的方法存储及调用进行讲解

结构体

  • 有如下结构体

    struct WSPerson {
        func speak() {
            print(" Hello word ")
        }
    }
    
    let ws = WSPerson()
    ws.speak()
    
    • 调用speak方法时查看它的汇编代码:

      截屏2021-12-17 13.46.51.png

    • 在汇编中,它是直接callq调用地址0x100003d20,也就是调用speak方法,这种调用也称作静态调用,因为结构体不存方法,所以调用时会直接在代码段(_TEXT)中读取。下面将项目的MachO文件在MachOView中打开

      截屏2021-12-17 14.15.33.png

    • 在代码段,我们就找到了要调用speak方法的汇编代码

  • 在断点查看汇编时,callq的地址后面显示的是符号,符号都存在字符串表(String Table),可以根据符号表(Symbol Table)中的信息读取,符号表查询过程如下:

    截屏2021-12-17 14.59.31.png

    • 符号在字符串表中的二进制如下:

    截屏2021-12-17 16.08.51.png

    • lddyld都会在link的时候读取符号表
  • 我们可以使用nm + MachO路径来查看项目的符号信息:

    截屏2021-12-17 16.15.53.png

  • 可以使用xcrun swift-demangle + 符号来还原符号:

    截屏2021-12-17 16.19.13.png

  • 下面来看下类的方法调用,先定义一个类及调用方法:

    class WSCat {
        func sleep1() { print(" sleeping 1.. ") }
        func sleep2() { print(" sleeping 2.. ") }
        func sleep3() { print(" sleeping 3.. ") }
        func sleep4() { print(" sleeping 4.. ") }
        func sleep5() { print(" sleeping 5.. ") }
    }
    
    let ragdoll = WSCat()
    ragdoll.sleep1()
    ragdoll.sleep2()
    ragdoll.sleep3()
    ragdoll.sleep4()
    ragdoll.sleep5()
    
    • 在调用方法处打上断点,再查看汇编:

      截屏2021-12-17 17.17.38.png

    • 可以看到callq的地址是一片连续的内存,应该是方法,进入第一个callq验证:

      截屏2021-12-17 17.19.07.png

  • 生产Sil文件并查看方法:

    截屏2021-12-17 17.29.46.png

    • Sil中的方法顺序与汇编中一致,这些方法都存在vtable中,下面我们去swift源码查看下vtable底层做了什么
  • swift源码中通过搜索initClassVTable,得到以下代码:

    截屏2021-12-17 18.21.21 2.png

    • 主要是通过指针平移获取方法名,并关联imp

extension

  • extension中的方法是怎样调度呢?下面定义WSCat类,然后Ragdoll类继承WSCat

    class WSCat {
        func sleep1() { print(" sleeping 1.. ") }
        func sleep2() { print(" sleeping 2.. ") }
        func sleep3() { print(" sleeping 3.. ") }
        func sleep4() { print(" sleeping 4.. ") }
    }
    
    extension WSCat {
        func sleep5() { print(" sleeping 5.. ") }
    }
    class Ragdoll: WSCat { }
    
    var cat = Ragdoll()
    cat.sleep5()
    
    • extension中的sleep5方法是怎么调度的呢,我们知道类里面的方法是通过vtable进行调度,下面生产Sil文件中查看vtable:

      截屏2021-12-21 13.58.00.png

    • Sil可以看到Ragdoll继承了WSCat中其他方法,但并没有sleep5方法。其实这个也比较好理解,假如sleep5也在WSCatvtable里,那么Ragdoll肯定也会继承过来,但如果子类要继续添加方法时,由于方法在vtable中是通过指针平移的方式添加,所以此时编译器无法确定是在父类添加还是子类添加,所以是不安全的,那么extension中的方法只能是直接调用,下面打断点查看汇编验证下

    截屏2021-12-21 14.01.05.png

  • 此时我们可以得出结论:extension中的方法调用是直接调用

总结

  • 结构体的方法调度是通过地址直接调用
  • 的方法调度是通过vtable来进行的
  • extension中的方法是直接调用

final,@objc,dynamic

  • 下面研究几个关键字,对方法调度的影响

final

  • 下面定义WSCat类,其中的一个方法使用final修饰

    class WSCat {
        final func sleep1() { print(" sleeping 1.. ") }
        func sleep2() { print(" sleeping 2.. ") }
        func sleep3() { print(" sleeping 3.. ") }
        func sleep4() { print(" sleeping 4.. ") }
    }
    
    • 然后结合Sil汇编分析方法调度

    截屏2021-12-21 14.35.19.png

    截屏2021-12-21 14.35.06.png

  • 所以得出结论:final修饰的方法是直接调用

@objc

  • WSCat类的其中方法中添加@objc关键字:

    class WSCat {
        @objc func sleep1() { print(" sleeping 1.. ") }
        func sleep2() { print(" sleeping 2.. ") }
        func sleep3() { print(" sleeping 3.. ") }
        func sleep4() { print(" sleeping 4.. ") }
    }
    
    • 结合Sil和汇编分析:

    截屏2021-12-21 15.14.55.png

    截屏2021-12-21 15.13.49.png
    截屏2021-12-21 15.14.55.png

    • 虽然vtable中有sleep1方法,但是调度方式与上面不同,这种调度方式叫函数表调度
  • 那么添加@objc的方法能被OC调用吗?其实不一定,我们可以先查看混编的头文件

    截屏2021-12-21 15.21.36.png

    • 结果头文件里并没有WSCat相关的信息,是因为 想要OC调用,类必须继承NSObject,将类继承NSObject然后在查看头文件

      截屏2021-12-21 15.24.56.png

  • 类继承NSObject后,我们来看看Sil文件有什么变化

    截屏2021-12-21 15.28.09.png
    截屏2021-12-21 15.28.30.png

    • 通过观察发现Sil中有两个sleep1方法,一个给Swift使用,带@objc标记的供给OC使用

dynamic

  • WSCat中的一个方法添加dynamic修饰
    class WSCat {
        dynamic func sleep1() { print(" sleeping 1.. ") }
        func sleep2() { print(" sleeping 2.. ") }
        func sleep3() { print(" sleeping 3.. ") }
        func sleep4() { print(" sleeping 4.. ") }
    }
    
    • 通过Sil和汇编分析得知dynamic修饰的函数调度方式是函数表调度

方法交换

  • Sil文件的sleep1函数位置,可以看到它被标记为dynamically_replacable

    截屏2021-12-21 16.00.58.png

    • 说明它是动态的可修改的,也就是如果类继承NSObject,则它可以进行method-swizzling
  • Swift中的方法交换需要使用@_dynamicReplacement(for: 调用的函数符号)函数,具体代码如下:

    class WSCat: NSObject {
        dynamic func sleep1() { print(" sleeping 1.. ") }
        func sleep2() { print(" sleeping 2.. ") }
        func sleep3() { print(" sleeping 3.. ") }
        func sleep4() { print(" sleeping 4.. ") }
    }
    
    extension WSCat {
        @_dynamicReplacement(for: sleep1)
        func eat() {  print(" have fish ")  } // 交换的函数
    }
    
    var cat = WSCat()
    cat.sleep1()
    
    • 打印结果如下:

    截屏2021-12-21 16.15.10.png

@objc+dynamic

  • dynamic的方法前面添加@objc关键字,代码如下:

    class WSCat: NSObject {
        @objc dynamic func sleep1() { print(" sleeping 1.. ") }
        func sleep2() { print(" sleeping 2.. ") }
        func sleep3() { print(" sleeping 3.. ") }
        func sleep4() { print(" sleeping 4.. ") }
    }
    
  • 调用sleep1然后查看汇编:

    截屏2021-12-21 16.24.05.png

    • 结果这个方法的调用方法变成了objc_msgSend

总结

  • struct值类型,它的函数调度是直接调用,即静态调度

    • 值类型在函数中如果要修改实例变量的值,则函数前面需要添加Mutating修饰
  • class引用类型,它的函数调度是通过vtable函数,即动态调度

  • extension中的函数是直接调用,即静态调度

  • final修饰的函数是直接调用,即静态调度

  • @objc修饰的函数是函数表调度,如果方法需要在OC中使用,则类需要继承NSObject

  • dynamic修饰的函数调度方式是函数表调度,它是动态可以修改的,可以进行method-swizzling

    • @objc+dynami修饰的函数是通过objc_msgSend来调用的
  • 如果函数中的参数想要被更改,则需要在参数的类型前面增加inout关键字,调用时需要传入参数的地址