Swift 的派发机制与思考题

3,244 阅读8分钟

简介

dispatch部分是根据其他人的文章整理的。

思考题部分是在项目中遇到,希望大家在看完派发后能有所思考。

dispatch介绍

我们都知道一个方法会在运行时被调用,一个方法被唤起,是因为编译器有一个计算机制,用来选择正确的方法,然后通过传递参数来唤起它.

这个机制通常被成为派发(dispatch). 分派就是处理方法调用的过程.

分派在处理方法调用的时候,可能会存在多个合理的可被调用的方法列表,此时就需要去选择最正确的方法.选择正确方法的整个过程,就是派发(dispatch). 每种编程语言都需要分派机制来选择正确的唤起方法.

方法从书写完成到调用完成,概括上会经历编译期和运行期两个阶段,而前面说的x选择哪个方法被执行,也是在这两个时期进行的.

选择正确方法的阶段,可以分为编译期和运行期,而分派机制通过这两个不同的时期分为两种: 静态分派(static dispatch)和 动态派发(dynamic dispatch).

static dispatch可以确保某个方法只有一种实现.static dispatch明显的快于dynamic dispatch,因为dynamic dispatch本身就意味着较高的性能开销.

dymanic dispatch,是基于运行期的给定信息来确定调用方法的,可能通过虚函数表实现,也可能借助其他的运行期的信息.

static dispatch

static dispatch是在编译期就完全确定调用方法的分派方式.

用于在多态情况下,在编译期就实现对于确定的类型,在函数调用表中推断和追溯正确的方法,包括列举泛型的特定版本,在提供的全部函数定义中选择的特定实现.

在编译器确定使用static dispatch后,会在生成的可执行文件内,直接指定包含了方法实现内存地址的指针,编译器直接找到相关指令的位置。当函数调用时,系统直接跳转到函数的内存地址执行操作。 这样的好处是,调用指令少,执行快,同时允许编译器能够执行例如内联等优化,缺点是由于缺少动态性而不支持继承。 事实上,编译期在编译阶段为了能够获取最大的性能提升,都尽量将函数静态化。

dynamic dispatch

dynamic dispatch是 用于在运行期选择调用方法的实现的流程. dynamic dispatch被广泛应用,并且被认为是面向对象语言的基本特性. OOP是通过名称来查找对象和方法的.但是多态就是一种特殊情况了,因为可能会出现多个同名方法,但是内部实现各不相同.如果把OOP理解为向对象发送消息的话.在多态模式下,就是程序向不知道类型的对象发送了消息,然后在运行期再将消息分派给正确的对象.之后对象再确定执行什么操作. 与static dispatch在编译期确定最终执行不同,dynamic dispatch的目的是为了支持在编译期无法确定最终最合适的实现的操作.这种情况一般是因为在运行期才能通过一个或多个参数确定对象的类型.例如 B继承自A, 声明var obj : A = B(),编译期会认为是A类型,但是真正的类型B,只能在运行期确定.

一种语言可能有多种dynamic dispatch的实现机制.语言的特性不同,动态分派的实现也各有差异.

消息机制派发(Message Dispatch)

消息机制派发 (Message Dispatch): Objc的函数派发都是基于消息派发的。这种机制极具动态性,既可以通过swizzling修改函数的实现,也可以通过isa-swizzling修改对象。

调用流程
通过消息派发执行子类中的函数的步骤: 1.到自己的方法列表中去找,如果找到了,执行对应逻辑,如果没找到执行2。 2.去它的父类中去找,发现找到了,就执行相应的逻辑。
派发流程

函数表派发 (Table Dispatch)

函数表派发是编译型语言实现动态行为最常见的实现方式。 函数表使用了一个数组来存储类声明的每一个函数的指针。大部分语言把这个称为 “virtual table”(虚函数表),Swift 里称为 “witness table”。 每一个类都会维护一个函数表,里面记录着类所有的函数,如果父类函数被override,表里面只会保存被 override 之后的函数。 一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数。

一个函数被调用时会先去读取对象的函数表 再根据类的地址加上该的函数的偏移量得到函数地址 最后跳到那个地址上去。 从编译后的字节码这方面来看就是两次读取一次跳转,比直接派发还是慢了些。

这种基于数组的实现, 缺陷在于函数表无法拓展. 子类会在虚数函数表的最后插入新的函数, 没有位置可以让 extension 安全地插入函数.

派发流程

编译器的优化

编译器可以通过whole module optimization检查继承关系,对某些没有标记final的类通过计算,如果能在编译期确定执行的方法,则使用Static dispatch。 比如一个函数没有 override,Swift 就可能会使用直接派发的方式,所以如果属性绑定了 KVO 它的 getter 和 setter 方法可能会被优化成直接派发而导致 KVO 的失效,所以记得加上 dynamic 的修饰来保证有效。

sil指令

编译器内部运行过程分为:语法分析,类型检查,SIL优化,LLVM后端处理。 这是生成sil文件的指令,可以通过查阅SIL官方文档了解文件中各种指令的含义。

swiftc -emit-sil main.swift | xcrun swift-demangle > main.sil

swiftc -emit-silgen -o main.swift | xcrun swift-demangle > main.silgen

源代码:

class A {
    let a = 1
    var b = 1
    private var c = 1
    
    func testA() {
        
    }
}
let a = A()
a.testA()

生成的sil 文件:

sil_stage canonical

import Builtin
import Swift
import SwiftShims

class A {
  @_hasInitialValue @_hasStorage final let a: Int { get }
  @_hasInitialValue @_hasStorage var b: Int { get set }
  @_hasInitialValue @_hasStorage private var c: Int { get set }
  func testA()
  init()
  @objc deinit
}

@_hasInitialValue @_hasStorage let a: A { get }

// a
sil_global hidden [let] @main.a : main.A : $A

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.a : main.A                  // id: %2
  %3 = global_addr @main.a : main.A : $*A        // users: %8, %7
  %4 = metatype $@thick A.Type                    // user: %6
  // function_ref A.__allocating_init()
  %5 = function_ref @main.A.__allocating_init() -> main.A : $@convention(method) (@thick A.Type) -> @owned A // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick A.Type) -> @owned A // user: %7
  store %6 to %3 : $*A                            // id: %7
  %8 = load %3 : $*A                              // users: %9, %10
  %9 = class_method %8 : $A, #A.testA!1 : (A) -> () -> (), $@convention(method) (@guaranteed A) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (@guaranteed A) -> ()
  %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'

......
sil_vtable A {
  #A.b!getter.1: (A) -> () -> Int : @main.A.b.getter : Swift.Int	// A.b.getter
  #A.b!setter.1: (A) -> (Int) -> () : @main.A.b.setter : Swift.Int	// A.b.setter
  #A.b!modify.1: (A) -> () -> () : @main.A.b.modify : Swift.Int	// A.b.modify
  #A.c!getter.1: (A) -> () -> Int : @main.A.(c in _12232F587A4C5CD8B1EEDF696793A4FC).getter : Swift.Int	// A.c.getter
  #A.c!setter.1: (A) -> (Int) -> () : @main.A.(c in _12232F587A4C5CD8B1EEDF696793A4FC).setter : Swift.Int	// A.c.setter
  #A.c!modify.1: (A) -> () -> () : @main.A.(c in _12232F587A4C5CD8B1EEDF696793A4FC).modify : Swift.Int	// A.c.modify
  #A.testA!1: (A) -> () -> () : @main.A.testA() -> ()	// A.testA()
  #A.init!allocator.1: (A.Type) -> () -> A : @main.A.__allocating_init() -> main.A	// A.__allocating_init()
  #A.deinit!deallocator.1: @main.A.__deallocating_deinit	// A.__deallocating_deinit
}

class_method是通过函数表派发的函数。

sil_vtable A是A中函数表的内容。

更多详情可以查阅文档。

思考题

第一题

只声明属性的情况下,会在函数表中生成方法么?有几种方法? 如果var、public、 final等属性修饰后会发生什么事?

第二题

static func exchangeFunc() {
        let originSelector = #selector(ASViewController.init(nibName:bundle:))
        let exchangeSelector = #selector(ASViewController.baiduInit(nibName:bundle:))
        guard let m1 = class_getInstanceMethod(self, originSelector) else {
            return
        }
        guard let m2 = class_getInstanceMethod(self, exchangeSelector) else {
            return
        }
        method_exchangeImplementations(m1, m2)
    }

这个是一个简单的方法交换的示例。 问题:init(nibName:bundle:)是一个初始化方法,为什么通过class_getInstanceMethod去获取而不是class_getClassMethod? 初始化方法是否有实例方法与类方法的区别?

第三题

class A {
    static func exchangeFunc() {
        let originSelector = #selector(A.printA)
        let exchangeSelector = #selector(A.printB)
        guard let m1 = class_getInstanceMethod(self, originSelector) else {
            return
        }
        guard let m2 = class_getInstanceMethod(self, exchangeSelector) else {
            return
        }
        method_exchangeImplementations(m1, m2)
    }
    
    @objc
    func printA() {
        print("A")
    }
    
    @objc
    func printB() {
        print("B")
    }
}

A.exchangeFunc()
let a = A()
a.printA() // A

问题:

为什么方法替换的操作在执行后没有生效?

如果希望生效应该怎么做?

第四题

class C1: UIView {
    
    func test1()
}

class C2: C1 {
    
    override func layoutSubview() {
//        ...
    }
    
    override func test1() {
        
    }
}

上面的方法派发机制是什么?

第五题

// Defined protocol。
protocol A {
    func a() -> Int
}
extension A {
    func a() -> Int {
        return 0
    }
}

// A class doesn't have implement of the function。
class B: A {}

class C: B {
    func a() -> Int {
        return 1
    }
}

B().a() 
C().a() 
(C() as A).a() 

分别写出以上方法运行后的输出,以及原因。

第六题

能否在override在extension中定义的方法?

能否在extension中override方法?

第七题

小张在工作中实现了类似于第五题的写法。

基类实现一个tableview并继承协议,且实现了其中必须实现的协议。

在子类中,override以及实现的协议或者添加新的tableview协议中的方法。

结果程序正常运行,并没有出现bug。

这是为什么?

突然有一天出现了bug,小张修改了Xcode某个属性,程序就能正常运行了。

修改了什么属性?