前言
本篇文章将会带大家分析Swift中结构体Struct
和类Class
的底层结构
,解释为什么结构体Struct
是值类型
,而类Class
是引用类型
?接着我们通过对Swift中的方法function
的调度分析,解释静态派发
与动态派发
的区别。
一、值类型
我们先通过一个示例看看,什么是值类型
?👇
func test(){
var age = 18
var age2 = age
age = 30
age2 = 45
print("age=\(age),age2=\(age2)")
}
test()
print("end")
断点run运行👇
上图可见,age2的值开始和age的值一样,但是后面改变了age2的值,age的值并未受到影响,接着我们lldb看看它们的内存地址👇
注意:可通过下面的指令查看某一变量的内存地址
po withUnsafePointer(to: &age){print($0)}
上图可见,age和age2的地址相差8字节,age的地址大于age2,是因为栈区是从高地址向低地址
分布内存地址空间的。
内存的分区可参考内存五大分区。
同时,age2的值的改变并不会影响age的值,说明var age2 = age
这句代码执行的是深拷贝
。所以,值类型有以下特点👇
-
地址中
存储的是值
- 值类型的传递过程中,相当于传递了一个副本,也就是所谓的
深拷贝
- 值传递过程中,
并不共享状态
1.1 Swift中的结构体
通常,定义结构体的属性时,可以给默认值,也可以不给👇
//***** 写法一 *****
struct LGTeacher {
var age: Int = 18
func teach(){
print("teach")
}
}
var t = LGTeacher()
//***** 写法二 *****
struct LGTeacher1 {
var age: Int
func teach(){
print("teach")
}
}
var t1 = LGTeacher1(age: 18)
写法二种不给属性默认值,编译器也不会报错。并且,初始化init方法
可以重写,也可以使用系统默认的。
1.1.1 查看SIL
基本操作,我们看看结构体在中间层SIL代码中是什么样的结构👇
上图红框处可见,系统会帮我们添加默认的初始化方法。
1.1.2 结构体是值类型
为什么说结构体
是值类型
,我们lldb看看内存就知道答案了👇
po withUnsafePointer(to: &t){print($0)}
上图中属性age的值
就存在结构体的地址
当中。和我们之前看的值类型的示例方法test
一样,再将t
的值赋给变量t1
,接着改变t1
结构体age
的值,那么t
的age值
会随着一起改变
吗?
var t = LGTeacher()
var t1 = t
t1.age = 20
运行👇
t
的age
值并没有变化
,仍然是18,所以可以验证--> 结构体是值类型
。
还是老套路,我们看看sil层代码👇
从上图sil中间层代码可知,t1
完全是alloc出来
的另一个内存地址
,和t的地址完全不同,t1和t并不共享状态
,所以将20赋给t1的age并不会影响到t的age。
二、引用类型
分析完结构体
,剩下就是类
了,类
究竟是不是引用类型
呢?
我们将上面的例子的struct 改成 class👇
上图可知,属性age如果没有给默认值,会报错:没有初始化方法!我们可以给一个可选类型?👇
所以,类class中的属性,要么给个默认值,要么指定是可选项类型。
2.1 类是引用类型?
我们可以通过示例验证一下👇
class LGTeacher {
var age: Int = 18
var age2: Int = 20
}
var t1 = LGTeacher()
首先lldb查看内存👇
我们接着查看0x0000000105905010这个地址里的信息👇
所以,引用类型有2个特点
- 地址中存储的是
堆区地址
-
堆区地址
中存储的是值
同样的问题,t1会影响t的值吗?
可见,t中和t1中存储的堆区地址是同一个
,所以修改t1会导致t一起变化,即浅拷贝
。
我们再改变下代码,看看下面的代码会怎么样?👇
class LGTeacher1 {
var age: Int = 18
var age2: Int = 20
}
struct LGTeacher {
var age: Int = 18
var age2: Int = 20
var teacher: LGTeacher1 = LGTeacher1()
}
var t = LGTeacher()
var t1 = t
t1.teacher.age = 30
print("t1.teacher.age = \(t1.teacher.age) t.teacher.age = \(t.teacher.age)")
我们在结构体中声明一个类的成员,t是结构体对象,t赋给t1,再改变t1中类成员的age属性值,t中的类成员age属性会变化吗?run👇
会一起改变
!同样,我们看看地址👇
上图可见,结构体t中成员变量是类class时,存的也是堆区的地址
。那么t赋给t1时,teacher成员当然是浅拷贝
。所以👇
在编写代码过程中,应该尽量
避免值类型包含引用类型
。
最后我们依然看看sil中间层代码👇
接着看看age的 getter
和 setter
👇
我们发现,getter
和 setter
中均进行了一次ref_element_addr
引用计数+1,那么,通过CFGetRetainCount
看看t.teacher
的引用计数是多少?👇
是3,这就解释了main中strong_retain
以及 getter
和 setter
中各一次的ref_element_addr
。
三、方法调度
上面讨论的值类型
和 引用类型
,都是针对成员变量或属性
,那么结构体或类的方法
,底层是如何调度的呢?接下来我们看看方法的调度。Swift方法的调用有2种方式:静态派发
& 动态派发
。
3.1 静态派发
什么是静态派发?值类型对象的方法调用是静态派发
,即直接对地址的调用
,这个地址就是函数的指针,而且在编译、链接完成后
就已经确定
了,存放在代码段
。例如:结构体是值类型,但是结构体的内部不存储方法
。
3.1.2 找入口
首先,在结构体方法调用时打上断点👇
查看汇编👇
我们发现了对teach()
方法的调用,在汇编层是callq 0x100003d00
,显然是对地址的直接操作。
接着我们查看Mach-O文件
👇
在Section64(__TEXT, __text)
区间中,确实有对地址0x100003d00
进行调用,该区间就是所谓的代码段
。接下来问题来了,底层是如何通过将地址0x100003d00
和 方法teach()
联系在一起的?也就是汇编层👇
上图红框处,汇编层代码中后面的符号,底层是如何解析出是结构体LGTeacher的teach()方法?
还是回到mach-O文件
中,可以看到👇
- Symbol Tables就是
符号表
- 符号表中并
不存储字符串
,字符串存储在String Table
字符串表中。 - 右侧第一个红框处,符号表中记录的
teach()函数
的字符串表
的偏移起始地址
是0000CA30
,偏移值Data是00000119
,然后计算得出的Value值就是工程名+类名+函数名
👇
-
右侧第2个红框处,是
teach()函数
在内存区间的定位,在Section64(__TEXT,__text)区间里,而函数的地址就是1000003D00
(右侧第3个红框处),👇
-
而
字符串表
中所记录的字符串值的地址则是👇
3.1.2 命令行查看符号表
我们还可以通过命令行查看以上teach()函数
的地址信息。
查看符号表:
nm mach-o文件路径
通过命令还原符号名称:xcrun swift-demangle 符号
示例中的符号表信息👇
其中,我们可以看到符号信息s9SwiftTest9LGTeacherV5teachyyF
,就是之前在符号表
中定位的工程名+类名+函数名
,还原其符号名称👇
Release模式的区别
注意:以上都是Debug模式下生成的Mach-O文件,如果是Release模式,会是什么样的情况呢?
首先,将edit scheme -> run
中的debug
改成release
,编译后查看,在可执行文件目录下,多一个后缀为dSYM的文件
👇
接着,去Mach-O文件中的符号表Symbol Table
中搜索teachyyF
,也就是teach()函数
👇
发现找不到
,其主要原因是👉 静态链接的函数,一旦编译完成,其地址确定后,当前的符号表
就会删除
当前函数对应的符号
,在release环境下,符号表
中存储的只是不能确定地址
的符号。
对于不能确定地址
的符号,是在运行时确定的
,即函数第一次被调用时
(相当于懒加载)。
扩展1:函数符号的命名规则
- C函数
对于C函数来说,命名的重整规则就是在函数名之前加_
,所以不支持函数重载
。
示例
#include <stdio.h>
void test(){ }
- OC方法
也不支持函数重载
,命名的重整规则是-[类名 函数名]
- Swift方法
支持函数重载
,Swift中的重整命名规则比较复杂,可以确保函数符号的唯一性。
扩展2:ASRL
地址空间布局随机化 address space layout randomizes
,简称ASRL。上述的teach()方法调用的汇编👇
对应的Mach-O的地址是👇
接着我们lldb image list
查看调用栈寄存器信息👇
其中0x0000000100000000
是程序运行的首地址
,后8位
是随机偏移00000000
,这就是ASLR
。将Mach-O
中的文件地址0x0000000100003D00 + 0x00000000 = 0x100003D00
,正好对应汇编里调用的地址。
3.2 动态派发
既然值类型对象
的方法调度是静态派发
,那么引用类型对象
的方法调度则是动态派发
了,所以我们以类Class
为例看看。
3.2.1 找入口
首先查看以下示例代码👇
class LGTeacher{
func teach(){}
func teach2(){}
func teach3(){}
func teach4(){}
init(){}
}
类LGTeacher
声明了4个方法,编译,查看Mach-O文件👇
上图红框处我们可以看到,类LGTeacher
声明的4个方法,它们的地址是连续的0x100039E0 --> 0x10003A00 --> 0x10003A20 --> 0x10003A40
。
不信?我们再看SIL中间层代码👇
swiftc -emit-sil xx.swift | xcrun swift-demangle >> ./xx.sil && vscode xx.sil
拉到最下面,发现有个sil_vtable LGTeacher
,你可以将其理解为类LGTeacher
的函数表
,这个函数表
也可以理解为数组
,为什么这么说是数组
呢?我们接下来看看函数表
底层的源码。
3.2.2 函数表底层的源码
我们在源码中搜索initClassVTable
,并加上断点,然后写上源码进行调试👇
源码中可见,其内部是通过for循环
编码,然后offset+index偏移
,然后获取method
,将其存入到偏移后的内存中,这里也可以验证函数是连续存放的
。
3.2.3 在extension中声明的方法
如果更改方法声明的位置呢?例如extension中的函数,此时的函数调度方式还是函数表调度
吗?我们可以通过代码验证一下👇
extension LGTeacher {
func teach5(){ print("teach5") }
}
接着定义一个子类LGStudent继承自LGTeacher👇
class LGStudent: LGTeacher{}
然后查看SIL中的V-Table👇
子类LGStudent只继承了LGTeacher class中定义的函数,即函数表中的函数
,并没有继承extension
中声明的方法,why?
原因是子类是将父类的函数表全部继承
了,此时子类增加函数
,那么就继续在连续的地址
中插入
,如果extension
函数也是在函数表中,则意味着子类也有extension
中声明的函数,但是子类并没有
相关的指针记录函数
是父类方法
还是 子类方法
,所以子类方法不知道该插入到哪里,导致extension
中的函数无法安全的放入子类
中,所以extension
中的方法是直接调用
的,且只属于当前类
,子类是无法继承
的。
那么我们在日常的开发过程中需要注意:
- 继承方法和属性,不能写
extension
中 -
extension
中创建的函数,一定是只属于当前类
,但是其子类也有其访问权限
,只是不能继承和重写
,例如👇
补充1:关键字 mutating & inout
先看下面代码
struct LGStack {
var items: [Int] = []
func push(_ item: Int){
items.append(item)
}
}
直接报错,原因是值类型
的结构体是不允许修改成员变量
的。接着修改代码👇
struct LGStack {
var items: [Int] = []
func push(_ item: Int){
print(item)
}
}
我们看看上面代码的SIL层代码👇
找到push
方法,发现self是let类型
,当然不允许修改
!
我们换一种写法👇
struct LGStack {
var items: [Int] = []
func push(_ item: Int){
var s = self
s.items.append(item)
}
}
var t = LGStack()
t.push(1)
运行看看👇
还是不能不能将item添加进去,因为s是值拷贝
,是另一个
结构体,此时调用push是将item添加到s
的数组中了,那么t
里当然没有。
根据编译器错误的提示,给push添加mutating
,发现可以添加到数组了
struct LGStack {
var items: [Int] = []
mutating func push(_ item: Int){
items.append(item)
}
}
运行👇
可以添加进去了,接下来看看SIL层代码,看看mutating
到底做了什么?
我没看到,mutating
对应的SIL层是inout
关键字,而self由之前的let类型变成了var类型。那么接下来我们看看inout
关键字。
inout
一般情况下,在函数的声明中,默认的参数都是不可变的
,如果想要直接修改,需要给参数加上inout
关键字。例如:
不加inout
关键字,直接报错!
添加inout关键字
,可以给参数赋值。
小结
- 结构体中的函数如果想修改其中的属性,需要在函数前加上mutating,而类则不用
- mutating本质也是加一个 inout修饰的self
- Inout相当于取地址,可以理解为地址传递,即引用
- mutating修饰方法,而inout 修饰参数
补充2:关键字 final、@objc、dynamic修饰函数
final
final 修饰的方法是直接调度
的,可以通过SIL + 断点验证
class LGTeacher {
final func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
init(){}
}
SIL代码👇
方法调度表里没有teach()方法。接下来打断点👇
查看汇编👇
汇编可知,teach()直接地址调用
,teach2() teach3() teach4()是地址偏移
的函数表调用
。
@objc
修改代码👇
class LGTeacher {
@objc func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
init(){}
}
查看SIL👇
teach()方法依旧在函数表中,那么@objc
修饰的方法是函数表调度
,再看汇编层👇
注意:如果只是通过
@objc修饰函数
,OC还是无法调用
swift方法的,class还需要继承NSObject
class LGTeacher : NSObject {
@objc func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
override init(){}
}
查看混编的头文件
打开往下拉👇
看到了系统自动转换的OC类LGTeacher和方法。接着我们看SIL👇
上图,一个是Swift的teach()
方法,一个是OC的teach()
方法。但是OC的teach()
方法内部是调用Swift的teach()
方法,代码👇
%2 = function_ref @main.LGTeacher.teach() -> () : $@convention(method) (@guaranteed LGTeacher) -> () // user: %3
%3 = apply %2(%0) : $@convention(method) (@guaranteed LGTeacher) -> () // user: %5
dynamic
修改代码👇
class LGTeacher : NSObject {
dynamic func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
override init(){}
}
查看SIL
teach()依旧是函数表调用。
使用dynamic
的意思是可以动态修改-->当类继承NSObject
时,可以使用method-swizzling
👇
swift中实现方法交换
class LGTeacher : NSObject {
dynamic func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
override init(){}
}
extension LGTeacher{
@_dynamicReplacement(for: teach)
func teach5(){
print("teach5")
}
}
// 调用代码
let t = LGTeacher()
t.teach()
run👇
我们注意到,在swift中的需要交换的函数前,需要使用dynamic修饰被交换的函数,然后通过@_dynamicReplacement(for: 被交换的函数符号)
进行交换。
@objc dynamic
再看一个例子,如果通过@objc dynamic
修饰的方法teach()👇
class LGTeacher : NSObject {
@objc dynamic func teach(){ print("teach") }
func teach2(){ print("teach2") }
func teach3(){ print("teach3") }
func teach4(){ print("teach4") }
@objc deinit{}
override init(){}
}
查看汇编👇
teach()方法调用走的是objc_msgSend
流程,即动态消息转发
。
总结
本篇文章重点讲解了结构体对象
和 类对象
的方法调度方式,结合示例代码,分析汇编层,SIL层代码,一一阐述了静态派发
和动态派发
的底层实现流程,然后解释了关于方法的一些常用关键字的含义,希望大家能够掌握。