方法调度是程序在调用方法时选择执行哪些指令的方式。这是每次调用方法时发生的事情,但不是你经常关心的事情。在编写性能代码时,了解方法调度工作是至关重要的,并且可以解释Swift中发现的一些令人困惑的行为。
编译的编程语言有三种主要的调度方法:direct dispatch、table dispatch和message dispatch。大多数语言支持其中的一个或两个。java默认使用table dispatch,但是你可以使用final关键字而去选择direct dispatch。C++默认使用direct dispatch,但你可以选择table dispatch通过添加virtual关键字。Objto-C总是使用message dispatch,但允许开发人员回落到C以获得direct dispatch的性能增益。Swift支持所有三种类型的dispatch。但对许多开发人员来说是一个难题,而且是大多数Swift开发者遇到的难题。
Types of Dispatch
Dispatch的目的是让程序告诉CPU,在内存中,它可以找到一个特定方法调用可执行代码。在深入研究Swift行为之前,让我们逐一查看三种Dispatch类型。每一个都有执行性能和动态行为之间的权衡。
Direct Dispatch
Direct Dispatch是最快的调度方式。它不仅使汇编指令数量最少,而且编译器可以执行各种智能技巧,如内联代码,以及超出该文档范围之外的更多事物。这通常被称为静态调度。然而,从编程的角度来看,直接调度也是最有限制性的,并且它不能动态支持子类化。
Table Dispatch
Table Dispatch是编译语言中最常见的动态行为实现。Table Dispatch对于类声明中的每个方法用函数指针数组表示。大多数语言都把它称为“virtual table”,但Swift使用“witness table”一词。每个子类都有自己的表副本,对于已重写的类每种方法都有不同的函数指针。当子类中添加新方法时,这些方法被追加到该数组的末尾。然后在runtime查阅此表以确定要运行的方法。
举一个
class ParentClass {
func method1() {}
func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
func method3() {}
}
在这种情况下,编译器将创建两个dispatch tables,一个用于ParentClass,另一个用于ChildClass:
let obj = ChildClass()
obj.method2()
当调用方法时,该过程将:
读取对象0xB00的dispatch
在方法的索引中读取函数指针。在这种情况下,function2的方法索引是1,因此读取地址0xB00 + 1。
跳转到地址0x222
表查找很简单,实现明智,性能特性是可预见的。然而,这种调度方法仍然比直接调度慢。从字节码的角度来看,有两个额外的读取和跳转,这会造成一些额外的性能开销。然而,这被认为是缓慢的另一个原因是编译器不能基于方法中正在发生的任何事情执行任何优化。
这种基于数组的实现的一个缺点是extensions不能扩展调度表。由于子类在调度表的末尾添加新的方法,所以extensions不能安全地添加函数指针的索引。
Message Dispatch
消息调度是可用的最为动态的调用方法。这是Cocoa开发的基础,是能够实现KVO、UIAppearance和Core Data等特征的机器。此功能的一个关键点在于,它允许开发人员在运行时修改调度行为。不仅可以通过旋转改变方法调用,而且对象可以通过isa-swizzling变成不同的对象,从而允许在面向对象的基础上自定义调度。
举个栗子
class ParentClass {
dynamic func method1() {}
dynamic func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
dynamic func method3() {}
}
Swift将这个层次结构建模为树形结构:
当发送消息时,runtime将遍历类层次结构,以确定要调用哪种方法。如果这听起来很慢,确实是这样!然而,这种查找是由一个快速缓存层守护的,一旦缓存被激活,查找就几乎与table dispatch一样快。
Swift Method Dispatch
那么,Swift中的dispatch方法又是怎样的呢?这里有四个方面,指导dispatch如何选择:
声明位置
引用类型
特定行为
能见度优化
在我定义这些之前,很重要的一点是,Swift并没有真正记录在使用table dispatch和使用message dispatch之间的区别。唯一的承诺是,动态关键字将通过Objective-C运行时使用message dispatch。
Location Matters
Swift有两个地方,可以让一个方法被声明:在一个类型的初始声明内,以及在一个扩展中。根据声明的类型,这将改变dispatch如何被执行。
class MyClass {
func mainMethod() {}
}
extension MyClass {
func extensionMethod() {}
}
在上面的示例中,mainMethod将使用table dispatch,extensionMethod将使用直接调度。当我第一次发现这一点时,我很惊讶。这些方法的行为是不易被察觉的。下面是基于引用类型和声明位置选择的调度类型的完整表:
这里有几点要注意:
值类型总是使用直接调度。
协议和类的扩展使用直接调度。
NSObject扩展使用消息调度
NSObject在初始声明中使用表调度方法!
初始协议声明中的方法的默认实现使用表调度。
Reference Type Matters
调用方法的引用的类型决定了调度规则。这似乎是显而易见的,但这是一个重要的区别。一个常见的混淆是当协议扩展和对象扩展都实现相同的方法时。
protocol MyProtocol {}
struct MyStruct: MyProtocol {}
extension MyStruct {
func extensionMethod() {
print("In Struct")
}
}
extension MyProtocol {
func extensionMethod() {
print("In Protocol") }
}
let myStruct = MyStruct()
let proto: MyProtocol = myStruct myStruct.extensionMethod() // -> “In Struct”
proto.extensionMethod() // -> “In Protocol”
许多人对Swift期望proto.extensionMethod()来调用strut的实现。但是,引用类型决定了调度选择,而protocol是使用direct dispatch。如果extensionMethod的声明被移动到协议声明中,则使用table dispatch,并使struct的实现被调用。此外,请注意,两个声明都使用direct dispatch,因此,给定direct dispatch 语义,预期的“override”行为是不可能的。这已经吸引了许多新的Swift开发人员失去警惕,因为这似乎是有Objective-C开发习惯的预期行为。
Specifying Dispatch Behavior
final
final允许对类中定义的方法进行direct dispatch。此关键字移除任何动态行为的可能性。它可以用于任何方法,甚至在extension中调度也可以直接进行。这也将从Objective-C 运行时隐藏该方法,并且不会生成选择器。
dynamic
dynamic允许在类中定义的方法上进行message dispatch。它还将方法可用在Objective-C的runtime中。要使用dynamic,必须Foundation,因为这包括NSObject和Objective-C runtime的核心。dynamic可用于允许在扩展中声明的方法被重写。dynamic关键字可以应用于NSObject的subclasses和直接Swift类。
@objc & @nonobjc
@Objc和@nonobjc更改了Objective-C runtime如何查看该方法。@Objc最常用的用法是命名空间选择器,比如@objc(abc_methodName)。@objc不改变调度选择,它只是使该方法对Objective-C 运行时可用。@nonobjc确实更改了发送选择。它可以用于禁用消息调度,因为它不将方法添加到消息调度依赖的Objective-C runtime。我不确定final是否有区别,因为在我所看到的用例中程序集看起来是一样的。在阅读代码时,我更喜欢看final,因为它使意图更加清晰。
final @objc
也可以将方法标记为final,并使该方法可用于@objc的message dispatch。这将引起使用direct dispatch的方法调用,并用Objective-C runtime注册选择器。这允许该方法响应perform(selector)和其他Objective-C特性,同时在直接调用时提供direct dispatch的性能。
@inline
Swift还支持@inline,它提供了编译器可以用来更改direct dispatch的提示。有趣的是,dynamic @inline(__always) func dynamicOrDirect() {}可以编译!它似乎只是一个提示,因为程序集显示该方法仍将使用message dispatch。这感觉像是未定义的行为,应该避免。
Visibility Will Optimize
Swift将尽可能优化方法调度。例如,如果您有一个从未被重写的方法,Swift会注意到这一点,如果可以的话,它将使用direct dispatch。这种优化在很大程度上是极好的,但往往会为难使用target / action模式的Cocoa程序员
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Sign In", style: .plain, target: nil, action: #selector(ViewController.signInAction) )
}
private func signInAction() {}
这里,编译器将生成一个错误:Argument of '#selector' refers to a method that is not exposed to Objective-C。当您记住Swift正在优化使用direct dispatch的方法时,这是有意义的。这里的修复非常简单:只需向声明添加“@objc”或“dynamic”,以确保它在Objective-C运行时保持可见。当使用UIAppearance时,也会发生这种类型的错误,这依赖于代理对象和NSInvocation.
当使用更动态的基础特征时,需要注意的另一件事是,如果不使用dynamic关键字,这种优化可以悄悄地破坏KVO。如果用KVO观察到属性,并将属性升级为direct dispatch,则代码仍然会编译,但是动态生成的KVO方法不会被触发。
Dispatch Summary
需要记住的规则很多,下面是以上的调度规则的摘要:
NSObject与dynamic behavior损耗
许多Cocoa开发人员评论了dynamic behavior的损耗。谈话很有趣,有很多要点出现了。我希望继续这一论点,并指出Swift的dispatch行为的几个方面,我认为这影响了其dynamic behavior,并提出了解决方案。
Table Dispatch in NSObject
上文中,提到NSObject类的初始声明中定义的方法使用table dispatch。我觉得这很混乱,很难解释,最后,这只是一个边际性能的改善。除此之外:
大多数NSObject子类位于很多obj_msgSend之上。我强烈怀疑,任何这些调度升级都将使实际中任何Cocoa子类的性能改善。
大多数Swift NSObject子类广泛地使用扩展,从而避免了这种升级。
最后,这只是另一个小细节,复杂的调度故事。
对NSObject特性进行调度升级
可见性能的改进是非常好的,而且我喜欢在可能的情况下如何快速更新dispatch。但是,在我的UIView子类颜色属性中有一个理论性能提升,打破了UIKit的一个既定模式,这对语言来说是不太好的的。
NSObject as a Choice
正如structs是静态调度的选择一样,NSObject是message dispatch的首选。现在,如果你要向一个新的Swift开发人员解释为什么是NSObject子类,你必须解释Objective-C和这门语言的历史。除了继承Objective-C代码库外,没有理由选择NSObject子类。
目前,Swift中NSObject的调度行为被描述为“complicated”,这是不理想的。我很想看到这种变化:当你创建NSObject子类时,它应该是一个你想要完全动态message dispatch的信号。
译自https://www.raizlabs.com/dev/2016/12/swift-method-dispatch/