在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
关键字,调用时需求传入参数的地址