swift派发

作者: 狗蛋的春天 | 来源:发表于2021-04-27 16:52 被阅读0次

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

    静态派发

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

    动态派发

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

    Table 派发 (表派发)

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

    Message 派发(消息派发)

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

    关于 Swift 派发的两个问题

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

    确定派发机制的方法

    作为怀疑者,我对问题的第二部分更感兴趣。提出一个假设很容易,但是要一直进行检验并不是一件容易的事。经过数小时的搜寻,我碰巧知道了SIL文档[3],该文档合理地解释了派发策略的存在。这是一个简短的摘要:
    如果函数使用表派发,那么它会出现在 vtable(或用于协议的 witness_table)中
    sil_vtable Animal {
         #Animal.makeSound!1: (Animal) -> () -> () : main.Animal.makeSound() -> () // Animal.makeSound()
        ......
    }
    如果函数是通过消息派发的,那么调用中应该出现关键字 volatile 。此外,你将找到两个标记 foreign 和 objc_method, 表明该函数/方法是使用 Objective-C 运行时调用的。
      %14 = class_method [volatile] %13 : $Dog, #Dog.goWild!1.foreign : (Dog) -> () -> (), $@convention(objc_method) (Dog) -> () 
    

    如果并没有出现上面两种情况,则说明该函数/方法是使用静态调度的。

    注意

    首先,Struct 或 任何值类型的函数必须静态派发。这是有道理的,因为它们永远不会被覆盖。
    明确执行
    具有 final 关键字的函数也会被静态派发。
    具有 dynamic 关键字的函数将通过消息传递派发 —— 从 Swift 4.0 开始
    带有 dynamic 关键字的函数对 Objective-C 是隐式可见的。同时,Swift 4 要求你使用 @objc 属性显式声明它。
    普通扩展(即没有 final、dynamic、@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 方法。
    

    原则是什么?

    优先考虑直接调用(静态派发)。
    如果需要覆盖,则表派发是下一个候选。
    需要对 Objective-C 进行覆盖和可见性吗?然后发送消息。
    另一个关键是明确性更好。。隐式推断(如带有 @objc 的扩展)可能会发生变化。
    以下是一些常见案例的总结。建议你通过读取生成的 SIL 来进行双重检查。
    
    直接调用    Table   Message
    明确执行    final, static   —   dynamic
    值类型 所有方法    —   —
    协议  拓展中的方法  定义的方法   —
    类   拓展中的方法  定义的方法   带有 @objc 的扩展
    

    结论

    在这篇文章中,我们了解了什么是方法派发,以及 Swift 中的不同派发类型。我们通过一些例子来了解 Swift 是如何解析特定函数    的。另外,通过阅读 SIL,我们收集了关于函数具体遵循哪种派发假设的证据。
    静态派发由于其出色的性能而变得越来越重要。这就是为什么 Swift 是 Swift(雨燕)(相对于 Objective-C —— 一种动态语言)。
    尽管消息派发的性能似乎比较差,但它提供了极大的灵活性,以至于支持一系列很酷的技术。
    理解方法派发至关重要。它不仅可以帮助编写更优的代码,还可以避免一些奇怪的错误。
    在上面提到的这些之中,我们抛弃了编译器的优化。编译器优化的代码的能力很大程度上取决于我们编写代码的方式 :)。
    

    转载的

    相关文章

      网友评论

        本文标题:swift派发

        本文链接:https://www.haomeiwen.com/subject/duhprltx.html