Swift 静态派发与动态派发
The following article is from iOS成长指北 Author iOS成长指北
如果你是 OOP 爱好者,那么这些派发方法(尤其是动态派发)对你来说可能并不陌生。
方法派发是一种有助于决定应该执行哪个操作的机制,或者更具体地说,应该使用哪个方法实现。
基本信息
首先,值类型和引用类型都支持静态派发。
但是,仅引用类型(即 Class
)支持动态派发。这样做的原因是,简而言之,对于动态性或动态派发而言,我们需要继承,而我们的值类型并不支持继承。
牢记这一点,让我们继续前进!
为了更全面了解,Swift 中并不只有 2 种(静态和动态)派发方法,而是 4 种派发方法——
inline 内联(最快) 静态派发 虚拟派发 动态派发(最慢)
由编译器来决定应该使用哪种派发方法,优先使用内联,然后再按需选择。
静态vs动态 或 Swift vs Objective-C
默认情况下,Objective-C 支持动态派发。这种派发技术以多态的形式为开发人员提供了灵活性!子类化和覆盖现有的方法和东西,在使用上很棒。但是,这是有代价的。
动态派发以恒定的运行时开销为代价提高了语言的表达能力。这意味着,在动态派发的情况下,对于每个方法调用,我们的编译器都必须查看我们所调用的 witness table(其他语言中的虚拟表或派发表),以检查特定方法的实现。编译器需要确定你是在引用父类的实现,还是在引用子类的实现。由于所有对象的内存都是在运行时分配的,因此编译器只能在运行时执行该检查。
但是,静态派发不存在此问题。使用这种派发技术,编译器在编译时就已经知道了某个方法会调用哪种方法实现。因此,编译器可以执行某些优化,甚至在可能的情况下甚至可以将代码转换为内联,从而使整体执行速度变得更快!
那么,在 Swift 中是如何实现以上两种派发的呢?
为了实现动态派发,我们使用继承,对基类进行子类化,然后重写基类的现有方法。另外,我们可以使用 dynamic
关键字,并且需要在它前面加上@objc
关键字,以便将我们的方法公开给 Objective-C 运行时要实现静态派发,我们需要使用 final
和static
关键字,因为两者都确保了类和方法不能被覆盖。
让我们深入研究
静态派发(或直接调用)
如上所述,与动态派发相比,它们的速度非常快,因为编译器能够在编译时定位指令所在的位置。因此,当函数被调用时,编译器直接跳转到函数的内存地址来执行操作。这将带来巨大的性能提升和某些编译器优化,比如内联。
动态派发
如前所述,在这种类型的派发中,是在运行时而不是在编译时选择实现,这会增加一些开销。
现在,你可能想知道,既然它是如此昂贵,为什么我们还要使用它,甚至是不得不使用它!
好吧,因为它的灵活性。实际上,大多数 OOP 语言都支持动态派发,因为它允许多态的存在。
现在有两种动态派发方式
Table 派发 (表派发)
这种派发技术利用一个表,该表是一组函数指针,称为 witness table(或虚拟表),以查找特定方法的实现。
那么,这个 witness table 是如何工作的呢?
每个子类都有自己对于此表的拷贝 对于此类重写的每个方法,此表都有不同的函数指针 当子类添加新方法时,这些方法指针将附加到此数组的末尾 最后,编译器在运行时使用此表来查看要为方法调用具体哪个实现
由于编译器必须从表中读取方法实现的内存地址,然后跳转到该地址,因此它需要执行额外两条指令,所以它比静态派发要慢,但仍比 Message 派发要快。
**注意:**我并不太确定,但是这种特殊的派发技术可以是虚拟派发,虽然它利用了虚拟表,但是我找不到具体的参考。
Message 派发(消息派发)
这种动态派发技术是目前最动态的(双关语)。实际上,它是如此出色(省去了优化部分),以至于 Cocoa 框架在其许多大型框架中(例如 KVO,Core Data 等)使用了它。
此外,它还支持方法 swizzling,通常这意味着使用这种技术,我们可以在运行时更改方法的功能。
但是,Swift 编译器并不提供这种现成的功能。而是利用 Objective-C 运行时来实现这种派发技术。
为了显式地使用这种派发方法,我们需要使用 dynamic
关键字。在 Swift 4.0
之前,每当我们使用 dynamic
时,都会隐式地添加 @objc
,但从swift4.0开始,我们需要显式地用 @objc
标记它,使我们的方法暴露给 Objective-C 运行时,从而进行消息派发。
因为我们使用的是 Objective-C 运行时,所以在派发消息时,运行时将抓取类的层次结构以确定调用哪个方法。这真的很慢。为了弥补它的性能,它提供了一个缓存,在某种程度下这有所不同。
除非我们显示标记了
dynamic
关键字,否则编译器将始终尝试将派发方法升级为静态派发。
示例
值类型
struct Person {
func isIrritating() -> Bool { } // Static
}
extension Person {
func canBeEasilyPissedOff() -> Bool { } // Static
}
由于 struct
和 enum
是值类型,并且不支持继承,因此编译器会将方法置于静态
派发,因为它知道这些永远不会被子类化。
Protocol(协议)
protocol Animal {
func isCute() -> Bool { } // Table
}
extension Animal {
func canGetAngry() -> Bool { } // Static
}
这里要注意的关键点是,在扩展中定义的任何方法都使用静态派发
Class
class Dog: Animal {
func isCute() -> Bool { } // Table
@objc dynamic func hoursSleep() -> Int { } // Message
}
extension Dog {
func canBite() -> Bool { } // Static
@objc func goWild() { } // Message
}
final class Employee {
func canCode() -> Bool { } // Static
}
正常的方法声明遵循与协议相同的原则 每当我们使用 @objc
向Objective-C
运行时显示声明一个方法时,该方法将使用消息派发然而,如果我们将一个类标记为 final
,那么这个类就不能被子类化,因此它的方法使用静态派发。
你可以参照下面的表格进行对照
直接调用 | Table | Message | |
---|---|---|---|
明确执行 | final , static | — | dynamic |
值类型 | 所有方法 | — | — |
协议 | 拓展中的方法 | 定义的方法 | — |
类 | 拓展中的方法 | 定义的方法 | 带有 @objc 的扩展 |
好的,你会相信我所说的一切,对吗?
那么如何证明这些方法实际上跟我所说的一样在调用对应的派发技术呢?
为此,我们必须了解 Swift Intermediate Language (SIL)[1]。通过我在网上的研究,我发现有一种方法——
如果函数使用表派发,那么它会出现在
vtable
(或witness_table
)中sil_vtable Animal {
#Animal.isCute!1: (Animal) -> () -> () : main.Animal.isCute() -> () // Animal.isCute()
......
}s如果一个函数/方法使用了消息派发,SIL 会在在调用中出现
volatile
关键字。另外,你将找到两个标记foreign
和objc_method
, 表明该函数/方法是使用 Objective-C 运行时调用的。%14 = class_method [volatile] %13 : $Dog, #Dog.goWild!1.foreign : (Dog) -> () -> (), $@convention(objc_method) (Dog) -> ()
```如果并没有出现上面两种情况,则说明该函数/方法是使用静态调度的。
好吧,本文就到这里了!我计划这是一个由两篇文章组成的系列文章,而下一篇文章(现在可以在此处[2]获得)将涉及通过测试用例进行静态和动态调度之间的性能比较。
内容启发:Swift 中的方法派发[3]——Thuyen’s corner。
参考资料
[1]Swift Intermediate Language (SIL):
https://github.com/apple/swift/blob/main/docs/SIL.rst
[2]Static Dispatch Over Dynamic Dispatch:
https://betterprogramming.pub/static-dispatch-over-dynamic-dispatch-a-performance-analysis-47f9fee3803a
[3]Method dispatch in Swift:
https://trinhngocthuyen.github.io/tech/method-dispatch-in-swift/
- EOF -
看完本文有收获?请分享给更多人
关注「 iOS大全 」加星标,关注 iOS 动态
点赞和在看就是最大的支持❤️