阅读 142

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

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

值类型

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

    func valueTest() {     var age1: Int = 18     var age2: Int = 20      var age3: Int = age1     age3 = 26 } valueTest() 复制代码

    • 打印三个临时变量的内存地址结果如下:

      截屏2021-12-14 15.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: ) 复制代码

    截屏2021-12-14 16.13.38.png

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

    • 两个结构体中的成员一个有值,但初始化方法就产生了不同,下面通过Sil文件查看源码

下面对有初始值类型的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

    截屏2021-12-15 11.29.40.png

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

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

总结:
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)     } } 复制代码

    截屏2021-12-15 22.25.57.png

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

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

    • 将方法里的修改变量值修改下:

  • 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

    截屏2021-12-15 22.47.25.png




    • 观察发现方法添加mutating后,有以下变化:

    • 所以值的修改直接修改的是person地址,所以可以修改成功

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

    1. 参数中的WSPerson增加了inout修饰

    1. self访问的是地址

    1. selfvar可变类型

  • 上面出现的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() 复制代码

    截屏2021-12-21 14.01.05.png

    • extension中的sleep5方法是怎么调度的呢,我们知道类里面的方法是通过vtable进行调度,下面生产Sil文件中查看vtable:

      截屏2021-12-21 13.58.00.png

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

  • 此时我们可以得出结论: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.. ") } } 复制代码

    截屏2021-12-21 14.35.19.png

    截屏2021-12-21 14.35.06.png

    • 然后结合Sil汇编分析方法调度

  • 所以得出结论: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.. ") } } 复制代码

    截屏2021-12-21 15.14.55.png

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

    • 虽然vtable中有sleep1方法,但是调度方式与上面不同,这种调度方式叫函数表调度

    • 结合Sil和汇编分析:

  • 那么添加@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关键字,调用时需要传入参数的地址


作者:无双3
链接:https://juejin.cn/post/7044074683976663076


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐