Swift进阶-值类型&引用类型&方法调度
在Swift
中,提到值类型
我们通常会想到struct
,而类是引用类型,那么结构体为什么是值类型,类为什么又是引用类型呢?本文将从结构体和类触发,来探究值类型和引用类型的区别
值类型
下面从一个案例来分析值类型:
func valueTest() { var age1: Int = 18 var age2: Int = 20 var age3: Int = age1 age3 = 26 } valueTest() 复制代码
打印三个临时变量的内存地址结果如下:
在打印结果中可以看到:
age1
和age3
的地址不同,且age3
赋值后他们值也不同,说明age3 = age1
的过程相当于深拷贝
,说明age
就是值类型0x7
开头的内存代表栈区,且栈区内存是连续的,关于内存方面可以参考 内存分区与布局
结构体
下面来定义两个结构体:
struct WSPerson { var age: Int = 18 } struct WSTeacher { var age: Int } // 初始化 var person = WSPerson() var teacher = WSTeacher(age: ) 复制代码
在
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 复制代码
两个结构体对象相关打印结果如下:
通过打印发现二者的值与地址都不同,地址中存储的直接是成员的值
再对
tony
和wushuang
进行Sil
分析创建内存的代码中主要是在
栈区
开辟内存,以及对成员变量的处理,所以 结构体是值类型在
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
相关信息结果如下:从打印内容可以看出
teacher
是指针,它指向的是类在堆区的首地址,从类里面可以读取到类的相关信息
class是引用类型分析
创建一个对象
teacher2
,并将teacher
赋值给它,打印相关信息结果如下:虽然新对象的地址不同,但他们所指向的堆区内存一致,所以他们操作的是
同一片内存空间
,我们可以通过打印二者的age
值来验证:结果两个
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 复制代码
打印两个对象结果如下:
虽然
person
是值类型,但里面的dog
是引用类型,他们操作的是同一片内存,所以两个对象中的dog.dogAge
值是一样的
Mutating & inout
在定义结构体时,在结构体的方法中不允许修改实例变量,如下图:
struct WSPerson { var age: Int = 0 func add(_ count: Int) { print(count) } } 复制代码
在
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) 复制代码
由于结构体是值类型,所以此时的
s
是深拷贝,改变的值是s
中的,与person
对象无关,所以此时打印依旧是0
打印结果如下:
将方法添上之前报错提示
mutating
,此时就可以修改实例变量的值:观察发现方法添加
mutating
后,有以下变化:所以值的修改直接修改的是
person
地址,所以可以修改成功生成
Sil
文件并查看add
方法参数中的
WSPerson
增加了inout
修饰self
访问的是地址self
是var
可变类型上面出现的
inout
有什么作用我们不得而知,下面通过案例来分析下由于参数都是
let
类型,所以不可以修改,此时可以加上inout
对参数进行修饰:参数添加
intout
后,则传入的参数就是地址,所以此时参数可以进行修改
方法调度
在上面分析中我们知道结构体是值类型,那么它的方法在哪呢?下面我们将对结构体和类的方法存储及调用进行讲解
结构体
有如下结构体
struct WSPerson { func speak() { print(" Hello word ") } } let ws = WSPerson() ws.speak() 复制代码
调用
speak
方法时查看它的汇编代码:在汇编中,它是直接
callq
调用地址0x100003d20
,也就是调用speak
方法,这种调用也称作静态调用,因为结构体不存方法,所以调用时会直接在代码段(_TEXT
)中读取。下面将项目的MachO
文件在MachOView
中打开在代码段,我们就找到了要调用
speak
方法的汇编代码在断点查看汇编时,
callq
的地址后面显示的是符号,符号都存在字符串表(String Table
),可以根据符号表(Symbol Table
)中的信息读取,符号表查询过程如下:ld
和dyld
都会在link
的时候读取符号表符号在字符串表中的二进制如下:
我们可以使用
nm + MachO路径
来查看项目的符号信息:可以使用
xcrun swift-demangle + 符号
来还原符号:
类
下面来看下类的方法调用,先定义一个类及调用方法:
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() 复制代码
在调用方法处打上断点,再查看汇编:
可以看到
callq
的地址是一片连续的内存,应该是方法,进入第一个callq
验证:生产
Sil
文件并查看方法:在
Sil
中的方法顺序与汇编中一致,这些方法都存在vtable
中,下面我们去swift
源码查看下vtable
底层做了什么在
swift
源码中通过搜索initClassVTable
,得到以下代码:主要是通过指针平移获取方法名,并关联
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
:在
Sil
可以看到Ragdoll
继承了WSCat
中其他方法,但并没有sleep5
方法。其实这个也比较好理解,假如sleep5
也在WSCat
的vtable
里,那么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.. ") } } 复制代码
然后结合
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.. ") } } 复制代码
虽然
vtable
中有sleep1
方法,但是调度方式与上面不同,这种调度方式叫函数表调度
结合
Sil
和汇编分析:那么添加
@objc
的方法能被OC
调用吗?其实不一定,我们可以先查看混编的头文件结果头文件里并没有
WSCat
相关的信息,是因为 想要OC
调用,类必须继承NSObject
,将类继承NSObject
然后在查看头文件类继承
NSObject
后,我们来看看Sil
文件有什么变化
通过观察发现
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
说明它是动态的可修改的,也就是如果类继承
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() 复制代码
打印结果如下:
@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
然后查看汇编:结果这个方法的调用方法变成了
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