Swift进阶:函数派发机制
静态派发 vs 动态派发
根据函数调用能否在编译时或运行时确定,可以将派发机制分成两种类型:
静态派发
:在编译期的时候,编译器就知道要为某个方法调用某种实现。因此, 编译器可以执行某些优化,甚至在可能的情况下,可以将某些代码转换成inline
函数,从而使整体执行速度异常快。动态派发
:一定量的运行时开销为代价,提高了语言的灵活性。在动态派发机制下,对于每个方法的调用,编译器必须在方法列表(witness table
或virtial table
)中查找执行方法的实现,如在运行时判断选择父类的实现,还是子类的实现。由于对象的内存都是在运行时分配的,因此只能在运行时执行检查。
编译型语言的函数派发方式
- 直接派发(Direct Dispatch)
- 编译后就确定了方法的调用地址(也叫
静态派发
),汇编代码中,直接跳到方法的地址执行,生成的汇编指令最少,速度最快 - 例如C语言,C++默认也是直接派发
- 由于缺乏动态性,无法实现多态
- 编译后就确定了方法的调用地址(也叫
- 函数表派发(Table Dispatch)
- 在运行时通过一个函数表查找需要执行的方法,多一次查表的过程,速度比直接派发慢
- C++的虚函数(Virtual Table),维护一个虚函数表,对象创建的时候会保存虚表的指针,调用方法之前,从对象中取出虚表地址,根据编译时的方法偏移量从虚表取出方法的地址,跳到方法的地址执行
- 消息派发(Message Dispatch)
- 在OC中,方法调用被包装成消息,发给运行时(相当于
中间人
),运行时会找到类对象,类对象会保存类的数据信息,其中就包含方法列表(类方法在元类对象存储),或通过父类查找,直到命中执行,如果没找到方法,抛出异常。 - 运行时提供了很多动态的方法用于改变消息派发的行为,相比函数表派发有很强的
动态性
,由于运行时支持的功能很多,方法查找的过程比较长,性能比较低。
- 在OC中,方法调用被包装成消息,发给运行时(相当于
性能:直接派发 > 函数表派发 > 消息机制派发
函数表派发和消息派发属于动态派发
Swift支持上面三种函数派发方式,Swift编译器会根据不同的情况选择不同的派发方式,基于性能考虑优先选择性能高的派发方式。
Swift方法派发机制
这里先只讨论纯Swift对象(非继承自NSObject),继承自OC类的比较特殊,放到后面讨论
直接派发
在Swift中,下面方法会被编译为直接派发,在ARM64上调用方法会被编译为bl 函数地址
全局函数
使用
static
声明的所有方法使用
final
声明的所有方法,使用final
声明的类里面的所有方法1
2
3
4
5
6
7
8
9
10
11
12
13
14class ParentClass {
func method1() {}
func method2() {}
}
final class ChildClass: ParentClass {
override func method2() { }
func method3() {}
}
let obj = ChildClass()
// 下面调用都是直接派发
obj.method1()
obj.method2()
obj.method3()使用
private
声明的方法和属性,会隐式final
声明值类型
的方法,struct
和enum
都是值类型extension
中没有使用@objc
修饰的实例方法
函数表派发
引用类型的函数派发
只有引用类型才支持函数表派发,在Swift中,类的方法默认使用函数派发的方式,Swift的函数表叫
witness table
(其他语言叫virtual table
),特点如下:每个子类都有它自己的表结构
对于类中每个重写的方法,都有不同的函数指针
当子类添加新方法时,这些方法指针会添加在表数组的末尾
最后,编译器在运行时使用此表来查找调用函数的实现
1
2
3
4
5
6
7
8
9
10
11class ParentClass {
func method1() {}
func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
func method3() {}
}
let obj = ChildClass()
obj.method2()上述示例的内存结构示意图:
Protocol的函数派发
协议所指向的对象,只有在运行时才能确定类型,Swift对于协议默认都使用
函数表派发
,协议可以为struct
提供多态的支持1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17protocol Drawable {
func draw()
}
struct Line: Drawable {
func draw() {}
}
struct Point: Drawable {
func draw() {}
}
let drawable1: Drawable = Line()
let drawable2: Drawable = Point()
drawable1.draw() // 使用函数表派发
drawable1.draw() // 使用函数表派发
消息派发
Swift支持和OC混编,支持有限的Runtime运行时(主要是为了和OC混编);
对了纯Swift类,为了可以给OC调用,可以在方法前面加上dynamic
来支持消息派发(标记为dynamic
的变量/函数会隐式的加上@objc
关键字),它会使用OC的Runtime机制:
1 | class ParentClass { |
当消息被派发时,运行时会顺着继承关系向上查找被调用的方法,为了能够提升消息派发的性能,一般都会先查找缓存。
NSObject类的派发机制
这里指继承自NSObject的类(UIView, UIButton等)
- 对于普通的实例方法,使用函数表派发
- 对于使用
@objc
声明的方法,会暴露给ObjectiveC,在Swift中调用时是函数表派发,在OC中则是消息派发(参考Swift Runtime机制) - 对于
override
的OC方法,使用消息派发 - 使用
dynamic
修饰的方法使用消息派发 - 对于
extension
方法,默认使用直接派发
1 | class MyButton: UIButton { |
以上基于XCode11+Swift5测试,讨论的是未被编译器优化的情况,编译器会根据方法的使用情况做优化,函数表派发可能被优化成直接派发,部分方法会被优化成inline形式。