原文:Static Dispatch Over Dynamic Dispatch
作者:Shubham Bakshi
译者注: dispatch 可以理解为调度或者派发。参照
message-dispatch system
一般被译为消息派发系统本文中全部译为派发点赞评论,希望能在你的帮助下越来越好
作者:@iOS成长指北,本文首发于公众号 iOS成长指北,欢迎各位前往指正
如有转载需求请联系我,记住一定要联系哦!!!
这是我参与更文挑战的第1天,活动详情查看:更文挑战
方法派发是一个术语,是指程序确定应该执行哪个操作的机制(通过操作,这里指的是一组指令)。有时,我们仅希望在运行时确定具体的方法行为。这种机制导致了方法派发机制的不同,每种机制各有其优缺点。
静态派发
- 有时也被称为直接调用/派发。
- 如果一个方法是静态派发的,编译器就可以在编译时找到指令所在的位置。这样,当调用这种函数时,系统将直接跳转到此函数的内存地址以执行操作。这种直接行为导致执行速度非常快,并且还允许编译器执行各种优化,例如内联。实际上,由于性能的巨大提高,编译管道中存在一个阶段,在此阶段,编译器将在适用的情况下尝试使函数静态化。这种优化称为去虚拟化[1]。
动态派发
- 使用这种方法,程序直到运行时才知道要选择哪种实现。
- 尽管静态派发是极为轻量的,但它限制了灵活性,特别是在多态方面。这也是为什么动态派发被 OOP 语言广泛支持的原因
- 每种语言都有其自己的机制来支持动态调度。 Swift提供了两种实现动态性的方法:table 派发(表派发)和message 派发(消息派发)。
Table 派发 (表派发)
- 这是编译语言中最常见的选择。通过这种方法,一个类与一个所谓的 virtual table 相关联,虚拟表包含了指向对应于该类的实际实现的函数指针数组。
- 请注意,vtable 是在编译时构造的。因此,与静态派发相比,真多了两个附加指令(
read
和jump
)。从理论上讲,表派发应该也很快。
Message 派发(消息派发)
- 实际上,正是由 Objective-C 提供的这种机制(有时这被称作消息传递[2]),Swift 代码仅使用了 Objective-C 运行库。每次调用 Objective-C 方法时,调用都会传递给
objc_msgSend
,由后者负责处理查找工作。从技术上讲,运行时从给定类开始,抓取类的层次结构以便确认调用哪个方法。 - 与表派发不同的是,message passing dictionary 在运行时可以发生修改,从而使我们能够在运行时调整程序的行为。利用这一特点,Method swizzling 成为最流行的技术。
- 消息派发是三种派发(实际上是 4 种)中最具动态性的。作为交换,尽管系统通过实现缓存机制来保障查找的性能,但是其解决实现的成本可能会稍微高一些。
- 这种机制是 Cocoa 框架的基石。查看代码 Swift 的源码,你会发现 KVO 就是利用 swizzling 实现的。
关于 Swift 派发的两个问题
对于一个给定函数,它使用了什么样的派发方式?证据是什么?
确定派发机制的方法
作为怀疑者,我对问题的第二部分更感兴趣。提出一个假设很容易,但是要一直进行检验并不是一件容易的事。经过数小时的搜寻,我碰巧知道了SIL文档[3],该文档合理地解释了派发策略的存在。这是一个简短的摘要:
-
如果函数使用表派发,那么它会出现在
vtable
(或用于协议的witness_table
)中sil_vtable Animal { #Animal.makeSound!1: (Animal) -> () -> () : main.Animal.makeSound() -> () // Animal.makeSound() ...... } 复制代码
-
如果函数是通过消息派发的,那么调用中应该出现关键字
volatile
。此外,你将找到两个标记foreign
和objc_method
, 表明该函数/方法是使用 Objective-C 运行时调用的。参考[4]%14 = class_method [volatile] %13 : $Dog, #Dog.goWild!1.foreign : (Dog) -> () -> (), $@convention(objc_method) (Dog) -> () 复制代码
-
如果并没有出现上面两种情况,则说明该函数/方法是使用静态调度的。
琐事
-
首先,Struct 或 任何值类型的函数必须静态派发。这是有道理的,因为它们永远不会被覆盖。
-
明确执行
-
具有
final
关键字的函数也会被静态派发。 -
具有
dynamic
关键字的函数将通过消息传递派发 —— 从 Swift 4.0 开始带有
dynamic
关键字的函数对 Objective-C 是隐式可见的。同时,Swift 4 要求你使用@objc
属性显式声明它。
-
-
普通扩展(即没有
final
、dynamic
、@objc
)是直接派发的。现在,回想一下你可能曾经遇到过的编译错误:declarations in extensions cannot override yet.
这是因为这些功能当然是遵循静态派发的。你可能会问:“如果我想让这些扩展成为动态的呢?”。你明白了!如果扩展名是动态的,那就可以覆盖它。
extension Animal { func eat() { } @objc dynamic func getWild() { } } class Dog: Animal { override func eat() { } // Compiled error! @objc dynamic override func getWild() { } // Ok :) } 复制代码
其他情况
protocol Noisy {
func makeNoise() -> Int // TABLE
}
extension Noisy {
func makeNoise() -> Int { return 0 } // TABLE
func isAnnoying() -> Bool { return true } // STATIC
}
class Animal: Noisy {
func makeNoise() -> Int { return 1 } // TABLE
func isAnnoying() -> Bool { return false } // TABLE
@objc func sleep() { } // Still TABLE
}
extension Animal {
func eat() { } // STATIC
@objc func getWild() { } // MESSAGE
}
复制代码
-
Noisy.isAnnoying()
和Animal.getWild()
是静态派发的,因为它们是扩展。 -
Noisy.makeNoise()
尽管具有默认实现,但仍使用表派发。 -
我们必须谨慎使用
isAnnoying()
。请考虑以下两种用法。animal2.isAnnoying()
选择协议扩展的实现(因为它是直接方法,不需要查找)。以这种方式使用可能是一个错误的来源let animal1 = Animal() print(animal1.isAnnoying()) // Value: false let animal2: Noisy = Animal() print(animal2.isAnnoying()) // Value: true 复制代码
反过来说,
animal1.makeNoise()
和animal2.makeNoise()
产生相同的结果,因为协议是通过查找表来解决的。被
@objc
关键字修饰的@objc func sleep()
意味着该函数对 Objective-C 可见。但这并不一定意味着派发时一定选择 Objective-C 方法来执行。从函数调用的 SIL(见下文),我们可以看到$@convention(method)
,这意味着选择 Swift 方法而不是 objc 方法。%9 = class_method %8 : $Animal, #Animal.sleep!1 : (Animal) -> () -> (), $@convention(method) (@guaranteed Animal) -> () // user: %10 复制代码
原则是什么?
- 优先考虑直接调用(静态派发)。
- 如果需要覆盖,则表派发是下一个候选。
- 需要对 Objective-C 进行覆盖和可见性吗?然后发送消息。
另一个关键是明确性更好。。隐式推断(如带有 @objc
的扩展)可能会发生变化。
以下是一些常见案例的总结。建议你通过读取生成的 SIL 来进行双重检查。
直接调用 | Table | Message | |
---|---|---|---|
明确执行 | final , static |
— | dynamic |
值类型 | 所有方法 | — | — |
协议 | 拓展中的方法 | 定义的方法 | — |
类 | 拓展中的方法 | 定义的方法 | 带有 @objc 的扩展 |
表1. Swift中方法派发摘要(从上到下阅读)。请注意,上面已经在显式强制中提到了一些情况,例如具有
@objc dynamic
的类扩展。许多博客文章将类分为两类:NSObject 子类与(常规)类。尽管 NSObject 继承了许多在 Objective-C 运行时之上编写的方法,但我认为没有理由将它们分开。
结论
在这篇文章中,我们了解了什么是方法派发,以及 Swift 中的不同派发类型。我们通过一些例子来了解 Swift 是如何解析特定函数的。另外,通过阅读 SIL,我们收集了关于函数具体遵循哪种派发假设的证据。
- 静态派发由于其出色的性能而变得越来越重要。这就是为什么 Swift 是 Swift(雨燕)(相对于 Objective-C —— 一种动态语言)。
- 尽管消息派发的性能似乎比较差,但它提供了极大的灵活性,以至于支持一系列很酷的技术。
- 理解方法派发至关重要。它不仅可以帮助编写更优的代码,还可以避免一些奇怪的错误。
- 在上面提到的这些之中,我们抛弃了编译器的优化。编译器优化的代码的能力很大程度上取决于我们编写代码的方式 :)。
最后,在更高的 Swift 版本中情况可能有所不同。别忘了检查这篇文章的有效性 ?
参考资料
Devirtualization in LLVM and Clang: blog.llvm.org/2017/03/dev…
message passing:en.wikipedia.org/wiki/Messag…
Swift Intermediate Language (SIL): github.com/apple/swift…
dynamic dispatch:github.com/apple/swift…
Method Dispatch in Swift – by Brian King:www.raizlabs.com/dev/2016/12…
The Case for Message Passing in Swift – by Michael Buckley:www.buckleyisms.com/home/2014/6…
[swift] Dynamic keyword – by Srdan:dev.srdanstanic.com/ios/swift/2…
Friday Q&A 2014-07-04: Secrets of Swift’s Speed:www.mikeash.com/pyblog/frid…
The Swift Programming Language (Swift 4): Declaration Modifiers:developer.apple.com/library/con…
如果你有任何问题,请直接评论,如果文章有任何不对的地方,请随意表达。如果你愿意,可以通过分享这篇文章来让更多的人发现它。
感谢你阅读本文! ?