一、方法
1.1 方法的声明
我们知道,在 Swift 中, class 声明类类型, struct 声明结构体。类类型属于引用类型,结构体属于值类型。那么对于他们在方法上面的区别有什么体现?
可以看到,同样的方法声明,结构体却会报错。这是为什么?
Q:为什么如下加上关键字 mutating 就可以了
A: 因为值类型属性不能被自身的实例方法修改。
1.2 解读 SIL 代码
那我们可以通过 SIL 文件来看一下加上关键字的区别:
不加 mutating 关键字:
加上 mutating 关键字:
可以看到,加上了关键字之后,对于最后一个参数(实际上就是 self)多了一个关键字 @inout。
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
也就是说,对于后者,方法中的自身(self)实际上传递的是已经初始化过的地址,而前者传递的是结构体本身。
用伪代码表示即为:
var p = Point()
//没有@inout 关键字
let x1 = p
//有@inout 关键字
var x2 = withUnsafePointer(to: &p){return $0}
p.x = 20
此时,x1 和 p 是两个独立的值,x2 就是 p 的指针地址,指向 p 这个值。所以实际上
x2.pointee = p
这就造就了 Swift 中的异变函数,异变函数的本质就是:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切。
同时,如果我们希望函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为 输入输出 形式参数 ,也就是加上 inout 关键字。
二、方法调度
0x10034763c <+28>: mov x0, #0x0
0x100347640 <+32>: bl 0x10034768c ; type metadata accessor for LGSwift.LGTeacher at <compiler-generated>
0x100347644 <+36>: mov x20, x0
0x100347648 <+40>: bl 0x1003475d0 ; LGSwift.LGTeacher.__allocating_init() -> LGSwift.LGTeacher at ViewController.swift:11
0x10034764c <+44>: mov x20, x0
0x100347650 <+48>: str x20, [sp, #0x8]
0x100347654 <+52>: mov x0, x20
0x100347658 <+56>: bl 0x100349e5c ; symbol stub for: swift_retain
0x10034765c <+60>: str x20, [sp, #0x10]
-> 0x100347660 <+64>: ldr x8, [x20]
0x100347664 <+68>: ldr x8, [x8, #0x50]
0x100347668 <+72>: blr x8
0x10034766c <+76>: ldr x0, [sp, #0x8]
0x100347670 <+80>: bl 0x100349e50 ; symbol stub for: swift_release
0x100347674 <+84>: ldr x0, [sp, #0x10]
0x100347678 <+88>: bl 0x100349e50 ; symbol stub for: swift_release
0x10034767c <+92>: ldp x29, x30, [sp, #0x30]
0x100347680 <+96>: ldp x20, x19, [sp, #0x20]
0x100347684 <+100>: add sp, sp, #0x40 ; =0x40
0x100347688 <+104>: ret
相信这样的汇编代码大家都很眼熟,我们在开发过程中不止一次的跳转到这样的界面。下面我们就通过汇编代码的相关知识一起来看方法调度的流程。
首先,汇编代码中常用的指令如下:
mov | 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址) |
add | 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中 |
sub | 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中 |
and | 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中 |
orr | 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中 |
str | 将寄存器中的值写入到内存中 |
ldr | 将内存中的值读取到寄存器中 |
cbz | 和 0 比较,如果结果为零就转移(只能跳到后面的指令) |
cbnz | 和非 0 比较,如果结果非零就转移(只能跳到后面的指令) |
cmp | 比较指令 |
bl | (branch)跳转到某地址(无返回) |
blr | 跳转到某地址(有返回) |
ret | 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中 |
2.1 类的方法调度
对于如下代码运行在真机上后,打断点:
//类
class Teacher {
func teach() {
print("teach")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
var t = Teacher()
t.teach()
}
}
开启 Xcode->Debug->Debug Workflow-> Always Show Disassembly 后,断点会进入汇编代码如下:
我们在初始化和释放中间找到 blr 这一句。 就代表 t.teach() 这个方法的执行。
如果在 blr 这一句加上断点,并且按住 control 键点击 step into,就可以发现这一句代码进入的就是 teach() 这个方法。
通过红框内的汇编代码的解释如下:
1.bl 0x1041c35d0 :跳转到 Teacher 的初始化方法中。
2.mov x20, x0 :将上一步初始化后的实例对象 x0 赋值给 x20(此时 x20 就是实例变量)
3.str x20 , [sp, #0x10] :将 x20的值保存到 sp + #0x10(偏移量)的内存处
4.ldr x8, [x20]:取 x20 的值(第一个 8 字节,也就是 metadata)放入寄存器 x8 中
5.ldr x8, [x8, #0x50]:将x8的值加上 0x50(偏移量)作为地址,将该地址的值放入寄存器 x8
6.blr x8:此时 x8 就是 teach()这个函数的内存地址。通过 blr 跳转执行。
所以函数的调用过程:先找到 metadata ——> 确定函数地址(metadata+偏移量) ——> 执行函数基于函数表的调度。
2.1 结构体的方法调度
//结构体
struct Teacher {
func teach() {
print("teach")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
var t = Teacher()
t.teach()
}
}
断点进入汇编的表示如下:
可以看到这里的方法调用就是直接的地址调用,也就是——静态派发
同样的,对于 extension 中声明的方法,他的调度方式也是静态派发。
class Teacher {
func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
}
extension Teacher {
func teach2() {
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
var t = Teacher()
t.teach()
t.teach1()
t.teach2()
}
}
在如上操作之后得到的汇编如下:
可以看到,这里的 teach2() 方法并不是通过 metadata + 偏移量的方式去寻找内存地址,而是直接通过内存地址去执行方法。
这是因为当代码编译到extension 这一步时,Teacher这个类的 V-Table 已经编译好了,所以没有办法再往里面找地址插入这个额外声明的方法,所以就将这个额外声明的方法直接存入内存地址中。
2.3 总结
类型 | 调度方式 | extension |
值类型 | 静态派发 | 静态派发 |
类 | 函数表派发 | 静态派发 |
NSObject 子类 | 函数表派发 | 静态派发 |
三、影响函数派发的方式
-
final:添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。
-
dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发
-
@objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
class Teacher {
final func teach() {
print("teach")
}
dynamic func teach1() {
print("teach1")
}
@objc func teach2() {
print("teach2")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
var t = Teacher()
t.teach()
t.teach1()
t.teach2()
}
}
另:对于 dynamic 修饰的方法。表示该方法为动态的。目的在于在外部可以调用 @_dynamicReplacement(for: ) 来替换这个方法。比如:
class Teacher {
dynamic func teach() {
print("teach")
}
}
extension Teacher {
@_dynamicReplacement(for: teach)
func teach3() {
print("teach3")
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
var t = Teacher()
t.teach()
}
}
以上代码执行结果输出的结果是 "teach3"。
- 一般来说,dynamic 会和 @objc 一起使用。他是消息派发方式。如:
class Teacher {
@objc dynamic func teach() {
print("teach")
}
}
四、函数内联
在项目中,通过 Target——>Build Settings ——>Swift Compiler可以设置优化模式:
函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
-
将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内
联函数作为优化。
-
always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
-
never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
-
如果函数很⻓并且想避免增加代码段大小,可以使用@inline(never)
另:如果对象只在声明的文件中可⻅,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性。