Method Dispatch in Swift

Objective-C 方法调用的方式是发消息,那 Swift 方法调用的方式是什么呢?搞清楚 Swift 中方法调用的方式,会对于我们理解 Swift 有更好的帮助 Based on Swift 4 & Xcode 9.2
先看一个最简单的例子,
// DogStruct.swift
struct DogStruct {
  func makeNoise() {
    print("bark in struct!")
  }
}

let ds = DogStruct()
ds.makeNoise()
 
我们如何知道 ds.makeNoise() 是如何调用的呢?是 objc_msgSend 吗?光靠猜可能不太行,这里我们就要通过 SIL 来分析一下了。SIL 能够体现 Swift 的实现细节,分析 SIL 能让我们更好地理解 Swift 代码执行的过程。
使用 swiftc 把上面这个文件处理一下,生成 DogStuct.sil
# 使用 swiftc 生成 SIL,不开启编译器优化
swiftc -emit-silgen DogStruct.swift -Onone > DogStruct.sil
打开 DogStruct.sil,刚才的代码被编译成了一个 90 几行的 SIL 文件,下面这几行代表我们的 main 函数(其它部分已省略,感兴趣的同学可以自己尝试生成),
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @_T06vtable2dsAA9DogStructVv       // id: %2
  %3 = global_addr @_T06vtable2dsAA9DogStructVv : $*DogStruct // users: %9, %7
  // function_ref DogStruct.init()
  %4 = function_ref @_T06vtable9DogStructVACycfC : $@convention(method) (@thin DogStruct.Type) -> DogStruct // user: %6
  %5 = metatype $@thin DogStruct.Type             // user: %6
  %6 = apply %4(%5) : $@convention(method) (@thin DogStruct.Type) -> DogStruct // user: %7
  store %6 to [trivial] %3 : $*DogStruct          // id: %7
  // function_ref DogStruct.makeNoise()
  %8 = function_ref @_T06vtable9DogStructV9makeNoiseyyF : $@convention(method) (DogStruct) -> () // user: %10
  %9 = load [trivial] %3 : $*DogStruct            // user: %10
  %10 = apply %8(%9) : $@convention(method) (DogStruct) -> ()
  %11 = integer_literal $Builtin.Int32, 0         // user: %12
  %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
  return %12 : $Int32                             // id: %13
} // end sil function 'main'
其中 4 ~ 10 行是 let ds = DogStruct() 的过程。程序先为 ds 创建了一块全局的空间,然后用 %3 指向它;拿到 DogStruct.init() 的函数引用(function_ref%4)以及 DogStruct.Type (%5),调用之前获取到的 init() 方法并获取到返回值 %6,最后把返回值(%6)存储到之前开辟的内存空间(%3)中
11 ~ 14 代表调用 ds.makeNoise() 的过程。%8DogStruct.makeNose() 的函数引用(function_ref),在调用函数之前,创建一个临时变量 %9 并指向 ds,调用 %8 DogStruct.makeNoise()
let ds = DogStruct(); ds.makeNoise() 这两句操作分别调用了两个函数。我们知道,在调用函数之前,需要先找到这个函数。通过生成的 SIL 可知,这两个函数的都是在编译时期就被确定的 (function_ref) ,这种调用方式是 Static Dispatch(静态调用)
在 Swift 中,除了 Static Dispatch,还有 Dynamic Dispatch(动态调用,被调用的函数在 runtime 才能被确认),而 Dynamic Dispatch 实现在方式又有 V-Table Dispatch、Witness Table Dispatch 和 objc_msgSend。虽然方法的调用方式有很多,但搞清楚方法调用的方式却没那么复杂
notion image

方法的派发方式

非 Protocol 对象调用的情况

通过观察编译 Swift 生成的 SIL 代码,总结了定义在 struct、class 和 NSObject Subclass 的常见方法的调用方式(在没有编译器优化的情况下),
notion image
我们可以通过现象简单推一下原因,为什么方法会以不同的方式被调用?

Struct

因为 stuct 不支持继承,所以它不需要一个 table 来记录方法信息。所以在方法调用者是一个 struct 的前提下,它所有的方法调用(包括协议方法),都是静态调用。
虽然不支持继承,但 struct 能通过 Protocol 实现多态

Class

对于一个 pure Swift class 来说,影响它方法调用的关键字只有 final
函数如果被标记成 final ,编译器就会知道这个方法不会被 override,并把它的调用方式标记成静态调用。而对于未标记成 final 并在 class 内部(非 extension)中定义的方法,Swift 会用一种叫作 Virtual Table 的机制来查找这个方法并调用
因为定义在 extension 中的方法目前还不支持 override,所以定义在其中的方法都是静态派发的。

NSObject Subclass

影响这种类型的函数调用方式的关键字有很多
标记为 final 的函数是一定会静态调用的,原因同 class。
主类(非 extension)中定义的普通方法和标记为 @objc 的方法都使用 V-Table 机制派发。用 Swift 编写的类是不能被 Objective-C 继承的,@objc 只是把方法暴露给 Objective-C,并没有改变方法派发的本质
dynamic 的方法不管在主类还是 extension 中都是通过发消息动态调用的,因为 dynamic 就是干这个事儿的。
Extension 中的方法是无法基于 V-Table 派发的,被标记为 @objcdynamic 的又无法使用静态派发,所以只能基于 message 派发。
以上介绍的都是在没有编译器优化的情况下方法的派发方式。在有优化的情况下,编译器会尽可能地把基于 Table 机制派发的方法变成静态派发,有的方法甚至会就地展开,变成 inline 的形式,一切为了效率嘛

Protocol 对象调用的情况

用 Struct、Class 和 NSObject Subclass 分别实现了同一个协议,用本身的对象和协议对象调用协议中的方法。通过观察编译后的 SIL,我们可以得出以下结论,
notion image
用类本身的对象调用协议方法的时候,像我们上面发现的一样,该怎么派发还是怎么派发,跟正常的方法调用没有区别;但是当用协议对象调用协议方法的时,不管是结构体还是类,所有的方法都是使用一种基于 Witness Table 的形式派发
定义为 @objc 的协议方法会基于 message 派发的

总结

希望大家能通过此文了解使用 SIL 分析 Swift 的实现的方法,并在看到一个 Swift 方法就能想到,它究竟是基于什么方式进行派发
下一篇文章大概会介绍一下 V-Table 和 Witness Table 都是些什么东西(不太监的情况下
如果有什么写错的地方欢迎指出~

Reference


© Xinyu 2014 - 2024