在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文件查看源码
- 在
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-
两个结构体对象相关打印结果如下:
-
通过打印发现二者的值与地址都不同,地址中存储的直接是成员的值
-
-
再对
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) } }- 生成Sil文件并查看
add方法:
- 在
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)- 打印结果如下:
- 由于结构体是值类型,所以此时的
s是深拷贝,改变的值是s中的,与person对象无关,所以此时打印依旧是0
-
将方法添上之前报错提示
mutating,此时就可以修改实例变量的值:- 生成
Sil文件并查看add方法
- 观察发现方法添加
mutating后,有以下变化:-
- 参数中的
WSPerson增加了inout修饰
- 参数中的
-
self访问的是地址
-
self是var可变类型
-
- 所以值的修改直接修改的是
person地址,所以可以修改成功
- 生成
-
上面出现的
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.. ") } }- 结合
Sil和汇编分析:
- 虽然
vtable中有sleep1方法,但是调度方式与上面不同,这种调度方式叫函数表调度
- 结合
-
那么添加
@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关键字,调用时需要传入参数的地址