Swift进阶:函数派发机制

静态派发 vs 动态派发

根据函数调用能否在编译时或运行时确定,可以将派发机制分成两种类型:

  • 静态派发:在编译期的时候,编译器就知道要为某个方法调用某种实现。因此, 编译器可以执行某些优化,甚至在可能的情况下,可以将某些代码转换成inline函数,从而使整体执行速度异常快。
  • 动态派发:一定量的运行时开销为代价,提高了语言的灵活性。在动态派发机制下,对于每个方法的调用,编译器必须在方法列表(witness tablevirtial table)中查找执行方法的实现,如在运行时判断选择父类的实现,还是子类的实现。由于对象的内存都是在运行时分配的,因此只能在运行时执行检查。

编译型语言的函数派发方式

  1. 直接派发(Direct Dispatch)
    • 编译后就确定了方法的调用地址(也叫静态派发),汇编代码中,直接跳到方法的地址执行,生成的汇编指令最少,速度最快
    • 例如C语言,C++默认也是直接派发
    • 由于缺乏动态性,无法实现多态
  2. 函数表派发(Table Dispatch)
    • 在运行时通过一个函数表查找需要执行的方法,多一次查表的过程,速度比直接派发慢
    • C++的虚函数(Virtual Table),维护一个虚函数表,对象创建的时候会保存虚表的指针,调用方法之前,从对象中取出虚表地址,根据编译时的方法偏移量从虚表取出方法的地址,跳到方法的地址执行
  3. 消息派发(Message Dispatch)
    • 在OC中,方法调用被包装成消息,发给运行时(相当于中间人),运行时会找到类对象,类对象会保存类的数据信息,其中就包含方法列表(类方法在元类对象存储),或通过父类查找,直到命中执行,如果没找到方法,抛出异常。
    • 运行时提供了很多动态的方法用于改变消息派发的行为,相比函数表派发有很强的动态性,由于运行时支持的功能很多,方法查找的过程比较长,性能比较低。

性能:直接派发 > 函数表派发 > 消息机制派发

函数表派发和消息派发属于动态派发

Swift支持上面三种函数派发方式,Swift编译器会根据不同的情况选择不同的派发方式,基于性能考虑优先选择性能高的派发方式。

Swift方法派发机制

这里先只讨论纯Swift对象(非继承自NSObject),继承自OC类的比较特殊,放到后面讨论

直接派发

在Swift中,下面方法会被编译为直接派发,在ARM64上调用方法会被编译为bl 函数地址

  1. 全局函数

  2. 使用static声明的所有方法

  3. 使用final声明的所有方法,使用final声明的类里面的所有方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class ParentClass {
    func method1() {}
    func method2() {}
    }
    final class ChildClass: ParentClass {
    override func method2() { }
    func method3() {}
    }

    let obj = ChildClass()
    // 下面调用都是直接派发
    obj.method1()
    obj.method2()
    obj.method3()
  4. 使用private声明的方法和属性,会隐式final声明

  5. 值类型的方法,structenum都是值类型

  6. extension中没有使用@objc修饰的实例方法

函数表派发

  1. 引用类型的函数派发

    只有引用类型才支持函数表派发,在Swift中,类的方法默认使用函数派发的方式,Swift的函数表叫witness table(其他语言叫virtual table),特点如下:

    • 每个子类都有它自己的表结构

    • 对于类中每个重写的方法,都有不同的函数指针

    • 当子类添加新方法时,这些方法指针会添加在表数组的末尾

    • 最后,编译器在运行时使用此表来查找调用函数的实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class ParentClass {
      func method1() {}
      func method2() {}
      }
      class ChildClass: ParentClass {
      override func method2() {}
      func method3() {}
      }

      let obj = ChildClass()
      obj.method2()

      上述示例的内存结构示意图:

  2. Protocol的函数派发

    协议所指向的对象,只有在运行时才能确定类型,Swift对于协议默认都使用函数表派发,协议可以为struct提供多态的支持

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    protocol Drawable {
    func draw()
    }

    struct Line: Drawable {
    func draw() {}
    }

    struct Point: Drawable {
    func draw() {}
    }

    let drawable1: Drawable = Line()
    let drawable2: Drawable = Point()

    drawable1.draw() // 使用函数表派发
    drawable1.draw() // 使用函数表派发

消息派发

Swift支持和OC混编,支持有限的Runtime运行时(主要是为了和OC混编);

对了纯Swift类,为了可以给OC调用,可以在方法前面加上dynamic来支持消息派发(标记为dynamic的变量/函数会隐式的加上@objc关键字),它会使用OC的Runtime机制:

1
2
3
class ParentClass {
dynamic func method2() {}
}

当消息被派发时,运行时会顺着继承关系向上查找被调用的方法,为了能够提升消息派发的性能,一般都会先查找缓存。

NSObject类的派发机制

这里指继承自NSObject的类(UIView, UIButton等)

  • 对于普通的实例方法,使用函数表派发
  • 对于使用@objc声明的方法,会暴露给ObjectiveC,在Swift中调用时是函数表派发,在OC中则是消息派发(参考Swift Runtime机制)
  • 对于override的OC方法,使用消息派发
  • 使用dynamic修饰的方法使用消息派发
  • 对于extension方法,默认使用直接派发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MyButton: UIButton {
// 直接派发
final func method1() {}

// 直接派发
static func method2() {}

// 函数表派发
func method3() {}

// 函数表派发/消息派发
@objc
func method4() {}

// 消息派发
@objc dynamic
func method5() {}

// 消息派发
override func layoutSubviews() {
super.layoutSubviews()
}
}

extension MyButton {
// 直接派发
func method6() {}

// 直接派发
dynamic func method8() {}

// 消息派发
@objc func method7() {}
}

以上基于XCode11+Swift5测试,讨论的是未被编译器优化的情况,编译器会根据方法的使用情况做优化,函数表派发可能被优化成直接派发,部分方法会被优化成inline形式。