函数派发方式
能够在编译期确定执行方法的方式叫做静态分派 Static dispatch
,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派 Dynamic dispatch
。
静态分派(Static dispatch)
Static dispatch
更快,CPU
直接拿到函数地址并进行调用,而且静态分派可以进行内联等进一步的优化,使得执行更快速,性能更高。
使用 Static dispatch
代替 Dynamic dispatch
提升性能
我们知道Static dispatch
快于Dynamic dispatch
,如何在开发中去尽可能使用Static dispatch
。
-
inheritance constraints
继承约束我们可以使用
final
关键字去修饰Class
,以此生成的Final class
,使用Static dispatch
。 -
access control
访问控制private
关键字修饰,使得方法或属性只对当前类可见。编译器会对方法进行Static dispatch
。
编译器可以通过 whole module optimization
检查继承关系,对某些没有标记 final
的类通过计算,如果能在编译期确定执行的方法,则使用 Static dispatch
。 Struct
默认使用 Static dispatch
。
Swift 提供了更灵活的Struct
,用以在内存、引用计数、方法分派等角度去进行性能的优化,在正确的时机选择正确的数据结构,可以使我们的代码性能更快更安全。
如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料:https://gitee.com/Mcci7/i-oser 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障
你可能会问 Struct
如何实现多态呢?
答案是 protocol oriented programming
。
以上分析了影响性能的几个标准,那么不同的算法机制Class
,Protocol Types
和 Generic code
,它们在这三方面的表现如何,Protocol Type
和 Generic code
分别是怎么实现的呢?我们带着这个问题看下去。
Protocol Type
这里我们会讨论Protocol Type如何存储和拷贝变量,以及方法分派是如何实现的。不通过继承或者引用语义的多态:
protocol Drawable {
func draw()
}
struct Point :Drawable {
var x, y:Double
func draw() { … }
}
struct Line :Drawable {
var x1, y1, x2, y2:Double
func draw() {
…
}
}
var drawables:[Drawable] //遵守了Drawable协议的类型集合,可能是point或者line
for d in drawables {
d.draw()
}
复制代码
因为 Point 和 Line 的尺寸不同,数组存储数据实现一致性存储,使用了Existential Container
。查找正确的执行方法则使用了 Protoloc Witness Table
。
以上通过 Protocol Type
实现多态,几个类之间没有继承关系,故不能按照惯例借助 V-Table
实现动态分派。但是对于 swift 来说,class
类和 struct
结构体的实现是不同的,而属于结构体的协议Protocol
,可以拥有属性和实现方法,管理Protocol Type
方法分派的表就叫做Protocol Witness Table
。
Protocol Witness Table
和 V-table
一样,Protocol Witness Table
(简称 PWT
)内存储的是方法数组,里面包含了方法实现的指针地址,一般我们调用方法时,是通过获取对象的内存地址和方法的位移offset
去查找的.
Protocol Witness Table
是用于管理 Protocol Type
的方法调用的,在我们接触 swift 性能优化时,听到另一个概念叫做 Value Witness Table
(简称 VWT
),这个又是做什么的呢?
Value Witness Table
[图片上传失败...(image-1b0d39-1638867090452)]
用于管理任意值的初始化、拷贝、销毁。即对 Protocol Type
的生命周期进行专项管理
对于每一个类型(Int或者自定义),都在metadata中存储了一个VWT(用来管理当前类型的值)
Value Witness Table
和 Protocol Witness Table
通过分工,去管理 Protocol Type
实例的内存管理(初始化,拷贝,销毁)和方法调用。
动态分派
但是对于多态的情况,我们不能在编译期确定最终的类型,这里就用到了 Dynamic dispatch
动态分派。动态分派的实现是,每种类型都会创建一张表,表内是一个包含了方法指针的数组。动态分派更灵活,但是因为有查表和跳转的操作,并且因为很多特点对于编译器来说并不明确,所以相当于 block
了编译器的一些后期优化。所以速度慢于 Static dispatch
。
函数表派发(Table dispatch)
编译型语言中最常见的派发方式,既保证了动态性也兼顾了执行效率。
函数所在的类会维护一个“函数表”(虚函数表),存取了每个函数实现的指针。
每个类的 vtable 在编译时就会被构建,所以与静态派发相比多出了两个读取的工作:
- 读取该类的 vtable
- 读取函数的指针
优点:
- 查表是一种简单,易实现,而且性能可预知的方式。
- 理论上说,函数表派发也是一种高效的方式。
缺点:
- 与静态派发相比,从字节码角度来看,多了两次读和一次跳转。
- 与静态派发相比,编译器对某些含有副作用的函数无法优化。
- Swift 类扩展里面的函数无法动态加入该类的函数表中,只能使用静态派发的方式。
举个例子(只是一个示例):
class A {
func method1() {}
}
class B: A {
func method2() {}
}
class C: B {
override func method2() {}
func method3() {}
}
复制代码
复制代码
offset | 0xA00 | A | 0xB00 | B | 0xC00 | C |
---|---|---|---|---|---|---|
0 | 0x121 | A.method1 | 0x121 | A.method1 | 0x121 | A.method1 |
1 | 0x222 | B.method2 | 0x322 | C.method2 | ||
2 | 0x323 | C.method3 |
let obj = C()
obj.method2()
复制代码
当method2
被调用时,会经历下面的几个过程:
- 读取对象
0xC00
的函数表 - 读取函数指针的索引,
method2
的地址为0x322
- 跳转执行
0x322
消息派发(Message dispatch)
熟悉 OC
的人都知道,OC
采用了运行时机制使用 obj_msgSend
发送消息,runtime
非常的灵活,我们不仅可以对方法调用采用 swizzling
,对于对象也可以通过 isa-swizzling
来扩展功能,应用场景有我们常用的 hook
和大家熟知的 KVO
。
大家在使用 Swift 进行开发时都会问,Swift 是否可以使用OC的运行时和消息转发机制呢?答案是可以。
Swift 可以通过关键字 dynamic
对方法进行标记,这样就会告诉编译器,此方法使用的是 OC
的运行时机制。
id returnValue = [obj messageName:param];
// 底层代码
id returnValue = objc_msgSend(obj, @selector(messageName:), param);
复制代码
复制代码
优点:
- 动态性高
- Method Swizzling
- isa Swizzling
- ...
缺点:
- 执行效率是三种派发方式中最低的
所幸的是 objc_msgSend 会将匹配的结果缓存到一个映射表中,每个类都有这样一块缓存。若是之后发送相同的消息,执行速率会很快。
总结来说,Swift 通过 dynamic
关键字的扩展后,一共包含三种方法分派方式:Static dispatch
,Table dispatch
和 Message dispatch
。下表为不同的数据结构在不同情况下采取的分派方式:
类型 | 静态派发 | 函数表派发 | 消息派发 |
---|---|---|---|
值类型 | 所有方法 | / | / |
协议 | extension | 主体创建 | / |
类 | extension/final/static | 主体创建 | @objc + dynamic |
NSObject子类 | extension/final/static | 主体创建 | @objc + dynamic |
如果在开发过程中,错误的混合了这几种分派方式,就可能出现 Bug
,以下我们对这些 Bug
进行分析:
此情况是在子类的 extension
中重载父类方法时,出现和预期不同的行为。
class Base:NSObject {
var directProperty:String { return "This is Base" }
var indirectProperty:String { return directProperty }
}
class Sub:Base { }
extension Sub {
override var directProperty:String { return "This is Sub" }
}
复制代码
执行以下代码,直接调用没有问题:
Base().directProperty // “This is Base”
Sub().directProperty // “This is Sub”
复制代码
间接调用结果和预期不同:
Base().indirectProperty // “This is Base”
Sub().indirectProperty // expected "this is Sub",but is “This is Base” <- Unexpected!
复制代码
在 Base.directProperty
前添加 dynamic
关键字就可以获得 "this is Sub" 的结果。Swift 在 extension 文档 中说明,不能在 extension
中重载已经存在的方法。
“Extensions can add new functionality to a type, but they cannot override existing functionality.”
会出现报错:Cannot override a non-dynamic class declaration from an extension
。
[图片上传失败...(image-c05dcd-1638867090451)]
出现这个问题的原因是,NSObject
的 extension
是使用的 Message dispatch
,而 Initial Declaration
使用的是 Table dispath
(查看上图)。extension
重载的方法添加在了 Message dispatch
内,没有修改虚函数表,虚函数表内还是父类的方法,故会执行父类方法。想在 extension
重载方法,需要标明dynamic
来使用 Message dispatch
。
协议的扩展内实现的方法,无法被遵守类的子类重载:
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi() {
print("Hello")
}
}
func greetings(greeter:Greetable) {
greeter.sayHi()
}
复制代码
现在定义一个遵守了协议的类 Person
。遵守协议类的子类 LoudPerson
:
class Person:Greetable {
}
class LoudPerson:Person {
func sayHi() {
print("sub")
}
}
复制代码
执行下面代码结果为:
var sub:LoudPerson = LoudPerson()
sub.sayHi() //sub
复制代码
不符合预期的代码:
var sub:Person = LoudPerson()
sub.sayHi() //HellO <-使用了protocol的默认实现
复制代码
注意,在子类。LoudPerson
中没有出现override
关键字。可以理解为LoudPerson
并没有成功注册Greetable
在Witness table
的方法。所以对于声明为Person
实际为LoudPerson
的实例,会在编译器通过Person
去查找,Person
没有实现协议方法,则不产生Witness table
,sayHi
方法是直接调用的。解决办法是在base类
内实现协议方法,无需实现也要提供默认方法。或者将基类标记为final
来避免继承
这里的确没有编译验证,感谢 MemoryReload 的实践验证,自己编译了源码之后纠正一下,这里的直接调用的确不是上述的原因:
- 在子类
LoudPerson
中实现了sayHi
方法,所以会直接调用LoudPerson
的sayHi
方法。 - 对于声明为
Person
实际为LoudPerson
的实例,会在编译器通过Person
去查找,声明在协议中的函数是使用函数表 vtable 派发的,从下面编译的 SIL 代码中也可以看到,Person
的函数表 vtable 中没有sayHi
方法,实际上,Person
类的sayHi
方法只是在接口扩展中进行了定义,没有最终的类型中实现。在使用时,因为Person
类只是一个符合Greetable
接口的实例,编译器对sayHi
唯一能确定的只是在接口扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。
import Foundation
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi()
}
func greetings(greeter: Greetable)
class Person : Greetable {
@objc deinit
init()
}
@_inheritsConvenienceInitializers class LoudPerson : Person {
func sayHi()
override init()
@objc deinit
}
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
...
} // end sil function 'main'
// Greetable.sayHi()
sil hidden @$s16FunctionDispatch9GreetablePAAE5sayHiyyF : $@convention(method) (@in_guaranteed Self) -> () {
// %0 "self" // user: %1
bb0(%0 : $*Self):
debug_value_addr %0 : $*Self, let, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.Word, 1 // user: %4
// function_ref _allocateUninitializedArray(_:)
%3 = function_ref @$ss27_allocateUninitializedArrayySayxG_BptBwlF : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %4
%4 = apply %3(%2) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %6, %5
%5 = tuple_extract %4 : $(Array, Builtin.RawPointer), 0 // user: %17
%6 = tuple_extract %4 : $(Array, Builtin.RawPointer), 1 // user: %7
%7 = pointer_to_address %6 : $Builtin.RawPointer to [strict] $*Any // user: %14
%8 = string_literal utf8 "Hello" // user: %13
%9 = integer_literal $Builtin.Word, 5 // user: %13
%10 = integer_literal $Builtin.Int1, -1 // user: %13
%11 = metatype $@thin String.Type // user: %13
...
} // end sil function '$s16FunctionDispatch9GreetablePAAE5sayHiyyF'
// protocol witness for Greetable.sayHi() in conformance Person
sil private [transparent] [thunk] @$s16FunctionDispatch6PersonCAA9GreetableA2aDP5sayHiyyFTW : $@convention(witness_method: Greetable) <τ_0_0 where τ_0_0 : Person> (@in_guaranteed τ_0_0) -> () {
// %0 // user: %2
bb0(%0 : $*τ_0_0):
// function_ref Greetable.sayHi()
%1 = function_ref @$s16FunctionDispatch9GreetablePAAE5sayHiyyF : $@convention(method) <τ_0_0 where τ_0_0 : Greetable> (@in_guaranteed τ_0_0) -> () // user: %2
%2 = apply %1<τ_0_0>(%0) : $@convention(method) <τ_0_0 where τ_0_0 : Greetable> (@in_guaranteed τ_0_0) -> ()
%3 = tuple () // user: %4
return %3 : $() // id: %4
} // end sil function '$s16FunctionDispatch6PersonCAA9GreetableA2aDP5sayHiyyFTW'
// LoudPerson.sayHi()
sil hidden @$s16FunctionDispatch10LoudPersonC5sayHiyyF : $@convention(method) (@guaranteed LoudPerson) -> () {
sil_vtable Person {
#Person.init!allocator: (Person.Type) -> () -> Person : @$s16FunctionDispatch6PersonCACycfC // Person.__allocating_init()
#Person.deinit!deallocator: @$s16FunctionDispatch6PersonCfD // Person.__deallocating_deinit
}
sil_vtable LoudPerson {
#Person.init!allocator: (Person.Type) -> () -> Person : @$s16FunctionDispatch10LoudPersonCACycfC [override] // LoudPerson.__allocating_init()
#LoudPerson.sayHi: (LoudPerson) -> () -> () : @$s16FunctionDispatch10LoudPersonC5sayHiyyF // LoudPerson.sayHi()
#LoudPerson.deinit!deallocator: @$s16FunctionDispatch10LoudPersonCfD // LoudPerson.__deallocating_deinit
}
sil_witness_table hidden Person: Greetable module FunctionDispatch {
method #Greetable.sayHi: (Self) -> () -> () : @$s16FunctionDispatch6PersonCAA9GreetableA2aDP5sayHiyyFTW // protocol witness for Greetable.sayHi() in conformance Person
}
复制代码
进一步通过示例去理解:
// Defined protocol。
protocol A {
func a() -> Int
}
extension A {
func a() -> Int {
return 0
}
}
// A class doesn't have implement of the function。
class B:A {}
class C:B {
func a() -> Int {
return 1
}
}
// A class has implement of the function。
class D:A {
func a() -> Int {
return 1
}
}
class E:D {
override func a() -> Int {
return 2
}
}
// Failure cases。
B().a() // 0
C().a() // 1
(C() as A).a() // 0 # We thought return 1。
// Success cases。
D().a() // 1
(D() as A).a() // 1
E().a() // 2
(E() as A).a() // 2
复制代码
如果对上述代码的执行结果理解的不到位的话,还可以借助喵神 PROTOCOL EXTENSION 里面的例子理解一下:
现在我们可以对一个已有的 protocol 进行扩展,而扩展中实现的方法将作为实现扩展的类型的默认实现。也就是说,假设我们有下面的 protocol 声明,以及一个对该接口的扩展:
protocol MyProtocol {
func method()
}
extension MyProtocol {
func method() {
print("Called")
}
}
复制代码
在具体的实现这个接口的类型中,即使我们什么都不写,也可以编译通过。进行调用的话,会直接使用 extension 中的实现:
struct MyStruct: MyProtocol {
}
MyStruct().method()
// 输出:
// Called in extension
复制代码
当然,如果我们需要在类型中进行其他实现的话,可以像以前那样在具体类型中添加这个方法:
struct MyStruct: MyProtocol {
func method() {
print("Called in struct")
}
}
MyStruct().method()
// 输出:
// Called in struct
复制代码
也就是说,protocol extension 为 protocol 中定义的方法提供了一个默认的实现。有了这个特性以后,之前被放在全局环境中的接受 CollectionType
的 map
方法,就可以被移动到 CollectionType
的接口扩展中去了:
extension CollectionType {
public func map(@noescape transform: (Self.Generator.Element) -> T) -> [T]
//...
}
复制代码
在日常开发中,另一个可以用到 protocol extension 的地方是 optional 的接口方法。通过提供 protocol 的 extension,我们为 protocol 提供了默认实现,这相当于变相将 protocol 中的方法设定为了 optional。关于这个,我们在可选接口和接口扩展一节中已经讲述过,就不再重复了。
对于 protocol extension 来说,有一种会非常让人迷惑的情况,就是在接口的扩展中实现了接口里没有定义的方法时的情况。举个例子,比如我们定义了这样的一个接口和它的一个扩展:
protocol A1 {
func method1() -> String
}
struct B1: A1 {
func method1() -> String {
return "hello"
}
}
复制代码
在使用的时候,无论我们将实例的类型为 A1 还是 B1,因为实现只有一个,所以没有任何疑问,调用方法时的输出都是 “hello”:
let b1 = B1() // b1 is B1
b1.method1()
// hello
let a1: A1 = B1()
// a1 is A1
a1.method1()
// hello
复制代码
但是如果在接口里只定义了一个方法,而在接口扩展中实现了额外的方法的话,事情就变得有趣起来了。考虑下面这组接口和它的扩展:
protocol A2 {
func method1() -> String
}
extension A2 {
func method1() -> String {
return "hi"
}
func method2() -> String {
return "hi"
}
}
复制代码
扩展中除了实现接口定义的 method1
之外,还定义了一个接口中不存在的方法 method2
。我们尝试来实现这个接口:
struct B2: A2 {
func method1() -> String {
return "hello"
}
func method2() -> String {
return "hello"
}
}
复制代码
B2
中实现了 method1
和 method2
。接下来,我们尝试初始化一个 B2
对象,然后对这两个方法进行调用:
let b2 = B2()
b2.method1() // hello
b2.method2() // hello
复制代码
结果在我们的意料之中,虽然在 protocol extension 中已经实现了这两个方法,但是它们只是默认的实现,我们在具体实现接口的类型中可以对默认实现进行覆盖,这非常合理。但是如果我们稍作改变,在上面的代码后面继续添加:
let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi
复制代码
a2
和 b2
是同一个对象,只不过我们通过 as
告诉编译器我们在这里需要的类型是 A2
。但是这时候在这个同样的对象上调用同样的方法调用却得到了不同的结果,发生了什么?
我们可以看到,对 a2
调用 method2
实际上是接口扩展中的方法被调用了,而不是 a2
实例中的方法被调用。我们不妨这样来理解:对于 method1
,因为它在 protocol
中被定义了,因此对于一个被声明为遵守接口的类型的实例 (也就是对于 a2
) 来说,可以确定实例必然实现了 method1
,我们可以放心大胆地用动态派发的方式使用最终的实现 (不论它是在类型中的具体实现,还是在接口扩展中的默认实现);但是对于 method2
来说,我们只是在接口扩展中进行了定义,没有任何规定说它必须在最终的类型中被实现。在使用时,因为 a2
只是一个符合 A2
接口的实例,编译器对 method2
唯一能确定的只是在接口扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。
也许在这个例子中你会觉得无所谓,因为实际中估计并不会有人将一个已知类型实例转回接口类型。但是要考虑到如果你的一些泛型 API 中有类似的直接拿到一个接口类型的结果的时候,调用它的扩展方法时就需要特别小心了:一般来说,如果有这样的需求的话,我们可以考虑将这个接口类型再转回实际的类型,然后进行调用。
整理一下相关的规则的话:
-
如果类型推断得到的是实际的类型
- 那么类型中的实现将被调用;如果类型中没有实现的话,那么接口扩展中的默认实现将被使用
-
如果类型推断得到的是接口,而不是实际类型
并且方法在接口中进行了定义,那么类型中的实现将被调用;如果类型中没有实现,那么接口扩展中的默认实现被使用
否则 (也就是方法没有在接口中定义),扩展中的默认实现将被调用
参考:
PROTOCOL EXTENSION
【基本功】深入剖析Swift性能优化
从SIL看Swift函数派发机制
作者:奉孝
链接:https://juejin.cn/post/7033682844581019656