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

值类型

  • 下面从一个事例来剖析值类型:

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

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

    • 在打印结果中能够看到: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文件检查源码

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

    • 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
    
    • 两个结构体目标相关打印结果如下:

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

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

  • 再对tonywushuang进行Sil剖析

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

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

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

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

总结:
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相关信息结果如下:

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

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

class是引证类型剖析

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

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

    • 虽然新目标的地址不同,但他们所指向的堆区内存一致,所以他们操作的是同一片内存空间,咱们能够经过打印二者的age值来验证:

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

    • 结果两个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
    
    • 打印两个目标结果如下:

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

    • 虽然person是值类型,但里边的dog是引证类型,他们操作的是同一片内存,所以两个目标中的dog.dogAge值是相同的

Mutating & inout

  • 在界说结构体时,在结构体的办法中不允许修正实例变量,如下图:

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

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

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

    • 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)
    
    • 打印结果如下:

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

    • 由于结构体是值类型,所以此刻的s是深复制,改动的值是s中的,与person目标无关,所以此刻打印依旧是0
  • 将办法添上之前报错提示mutating,此刻就能够修正实例变量的值:

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

    • 生成Sil文件并检查add办法

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

    • 调查发现办法增加mutating后,有以下改变:
        1. 参数中的WSPerson增加了inout润饰
        1. self访问的是地址
        1. selfvar可变类型
    • 所以值的修正直接修正的是person地址,所以能够修正成功
  • 上面呈现的inout有什么作用咱们不得而知,下面经过事例来剖析下

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

    • 由于参数都是let类型,所以不能够修正,此刻能够加上inout对参数进行润饰:

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

    • 参数增加intout后,则传入的参数便是地址,所以此刻参数能够进行修正

办法调度

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

结构体

  • 有如下结构体

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

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

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

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

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

  • 在断点检查汇编时,callq的地址后边显现的是符号,符号都存在字符串表(String Table),能够依据符号表(Symbol Table)中的信息读取,符号表查询进程如下:

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

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

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

    • lddyld都会在link的时候读取符号表
  • 咱们能够运用nm + MachO途径来检查项目的符号信息:

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

  • 能够运用xcrun swift-demangle + 符号来还原符号:

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

  • 下面来看下类的办法调用,先界说一个类及调用办法:

    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()
    
    • 在调用办法处打上断点,再检查汇编:

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

    • 能够看到callq的地址是一片接连的内存,应该是办法,进入第一个callq验证:

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

  • 出产Sil文件并检查办法:

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

    • Sil中的办法次序与汇编中一致,这些办法都存在vtable中,下面咱们去swift源码检查下vtable底层做了什么
  • swift源码中经过查找initClassVTable,得到以下代码:

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

    • 主要是经过指针平移获取办法名,并相关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:

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

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

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

  • 此刻咱们能够得出结论: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汇编剖析办法调度

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

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

  • 所以得出结论: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和汇编剖析:

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

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

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

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

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

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

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

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

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

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

    • 经过调查发现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

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

    • 说明它是动态的可修正的,也便是假设类承继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()
    
    • 打印结果如下:

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

@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然后检查汇编:

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

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

总结

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

    • 值类型在函数中假设要修正实例变量的值,则函数前面需求增加Mutating润饰
  • class引证类型,它的函数调度是经过vtable函数,即动态调度

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

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

  • @objc润饰的函数是函数表调度,假设办法需求在OC中运用,则类需求承继NSObject

  • dynamic润饰的函数调度办法是函数表调度,它是动态能够修正的,能够进行method-swizzling

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