[译] Swift 中的方法派发

原文:Static Dispatch Over Dynamic Dispatch

作者:Shubham Bakshi

译者注: dispatch 可以理解为调度或者派发。参照 message-dispatch system 一般被译为消息派发系统本文中全部译为派发

点赞评论,希望能在你的帮助下越来越好

作者:@iOS成长指北,本文首发于公众号 iOS成长指北,欢迎各位前往指正

如有转载需求请联系我,记住一定要联系哦!!!

这是我参与更文挑战的第1天,活动详情查看:更文挑战

方法派发是一个术语,是指程序确定应该执行哪个操作的机制(通过操作,这里指的是一组指令)。有时,我们仅希望在运行时确定具体的方法行为。这种机制导致了方法派发机制的不同,每种机制各有其优缺点。

静态派发

  • 有时也被称为直接调用/派发
  • 如果一个方法是静态派发的,编译器就可以在编译时找到指令所在的位置。这样,当调用这种函数时,系统将直接跳转到此函数的内存地址以执行操作。这种直接行为导致执行速度非常快,并且还允许编译器执行各种优化,例如内联。实际上,由于性能的巨大提高,编译管道中存在一个阶段,在此阶段,编译器将在适用的情况下尝试使函数静态化。这种优化称为去虚拟化[1]

动态派发

  • 使用这种方法,程序直到运行时才知道要选择哪种实现。
  • 尽管静态派发是极为轻量的,但它限制了灵活性,特别是在多态方面。这也是为什么动态派发被 OOP 语言广泛支持的原因
  • 每种语言都有其自己的机制来支持动态调度。 Swift提供了两种实现动态性的方法:table 派发(表派发)message 派发(消息派发)

Table 派发 (表派发)

  • 这是编译语言中最常见的选择。通过这种方法,一个类与一个所谓的 virtual table 相关联,虚拟表包含了指向对应于该类的实际实现的函数指针数组。
  • 请注意,vtable 是在编译时构造的。因此,与静态派发相比,真多了两个附加指令(readjump)。从理论上讲,表派发应该也很快。

Message 派发(消息派发)

  • 实际上,正是由 Objective-C 提供的这种机制(有时这被称作消息传递[2]),Swift 代码仅使用了 Objective-C 运行库。每次调用 Objective-C 方法时,调用都会传递给 objc_msgSend ,由后者负责处理查找工作。从技术上讲,运行时从给定类开始,抓取类的层次结构以便确认调用哪个方法。
  • 与表派发不同的是,message passing dictionary 在运行时可以发生修改,从而使我们能够在运行时调整程序的行为。利用这一特点,Method swizzling 成为最流行的技术。
  • 消息派发是三种派发(实际上是 4 种)中最具动态性的。作为交换,尽管系统通过实现缓存机制来保障查找的性能,但是其解决实现的成本可能会稍微高一些。
  • 这种机制是 Cocoa 框架的基石。查看代码 Swift 的源码,你会发现 KVO 就是利用 swizzling 实现的。

关于 Swift 派发的两个问题

对于一个给定函数,它使用了什么样的派发方式?证据是什么?

确定派发机制的方法

作为怀疑者,我对问题的第二部分更感兴趣。提出一个假设很容易,但是要一直进行检验并不是一件容易的事。经过数小时的搜寻,我碰巧知道了SIL文档[3],该文档合理地解释了派发策略的存在。这是一个简短的摘要:

  1. 如果函数使用表派发,那么它会出现在 vtable(或用于协议的 witness_table)中

    sil_vtable Animal {
    	#Animal.makeSound!1: (Animal) -> () -> () : main.Animal.makeSound() -> ()	// Animal.makeSound()
      ......
    }
    复制代码
  2. 如果函数是通过消息派发的,那么调用中应该出现关键字 volatile 。此外,你将找到两个标记 foreignobjc_method, 表明该函数/方法是使用 Objective-C 运行时调用的。参考[4]

    %14 = class_method [volatile] %13 : $Dog, #Dog.goWild!1.foreign : (Dog) -> () -> (), $@convention(objc_method) (Dog) -> () 
    复制代码
  3. 如果并没有出现上面两种情况,则说明该函数/方法是使用静态调度的。

琐事

  • 首先,Struct 或 任何值类型的函数必须静态派发。这是有道理的,因为它们永远不会被覆盖。

  • 明确执行

    • 具有 final 关键字的函数也会被静态派发。

    • 具有 dynamic 关键字的函数将通过消息传递派发 —— 从 Swift 4.0 开始

      带有 dynamic 关键字的函数对 Objective-C 是隐式可见的。同时,Swift 4 要求你使用 @objc 属性显式声明它。

  • 普通扩展(即没有 finaldynamic@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 版本中情况可能有所不同。别忘了检查这篇文章的有效性 ?

参考文章[5][6][7][8][9]希望能对你有所帮助

参考资料

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…


如果你有任何问题,请直接评论,如果文章有任何不对的地方,请随意表达。如果你愿意,可以通过分享这篇文章来让更多的人发现它。

感谢你阅读本文! ?

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享