swift中函数派发机制

必记(不废话)

  • 派发方式

    场景 \ 数据类型 值类型(枚举,结构体) 协议(Protocol) Class(Swift的类) NSObject子类
    默认 直接派发 函数表 函数表 函数表
    Extension(扩展) 直接派发 直接派发 直接派发 消息派发
  • 通过修饰符指定派发

    1. final

      • final允许类里面的函数使用直接派发. 这个修饰符会让函数失去动态性.
      • 任何函数都可以使用这个修饰符(包括本来就是直接派发的函数extension).
      • 这也会让 Objective-C 的运行时获取不到这个函数, 不会生成相应的 selector.
      • final修饰不能被继承;用final修饰方法方法不能被重写
    2. dynamic

      • dynamic 可以让类里面的函数使用消息机制派发.
      • 使用dynamic, 必须导入 Foundation 框架, 里面包括了 NSObjectObjective-C的运行时.
      • dynamic 可以让声明在 extension 里面的函数能够被 override.
      • dynamic 可以用在所有 NSObject 的子类和 Swift 的原声类.
    3. @objc & @nonobjc

      • @objc@nonobjc 显式地声明了一个函数是否能被Objective-C的运行时捕获到.
      • 使用 @objc 的典型例子就是给 selector 一个命名空间 @objc(abc_methodName), 让这个函数可以被 Objective-C 的运行时调用.
      • @nonobjc 会改变派发的方式, 可以用来禁止消息机制派发这个函数, 不让这个函数注册到 Objective-C 的运行时里. 我不确定这跟 final 有什么区别, 因为从使用场景来说也几乎一样. 我个人来说更喜欢 final, 因为意图更加明显.
      • 译者注: 我个人感觉, 这这主要是为了跟 Objective-C 兼容用的, final 等原生关键词, 是让 Swift 写服务端之类的代码的时候可以有原生的关键词可以使用.
    4. @objc final

      • 可以在标记为final的同时, 也使用 @objc 来让函数可以使用消息机制派发. 这么做的结果就是, 调用函数的时候会使用直接派发, 但也会在 Objective-C 的运行时里注册响应的 selector.
      • 函数可以响应 perform(selector:) 以及别的Objective-C特性, 但在直接调用时又可以有直接派发的性能.
    5. @inline

      • Swift 也支持 @inline, 告诉编译器可以使用直接派发. 有趣的是, dynamic @inline(__always) func dynamicOrDirect() {} 也可以通过编译! 但这也只是告诉了编译器而已, 实际上这个函数还是会使用消息机制派发. 这样的写法看起来像是一个未定义的行为, 应该避免这么做.

派发方式

  • 直接派发也称静态调用(Direct Dispatch)

    直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间, 例如函数内联等.

  • 函数表派发 (Table Dispatch )

    函数表使用了一个数组来存储类声明的每一个函数的指针. 每一个类都会维护一个函数表, 里面记录着类所有的函数, 如果父类函数被override的话, 表里面只会保存被override之后的函数. 一个子类新添加的函数,都会被插入到这个数组的最后. 运行时会根据这一个表去决定实际要被调用的函数.

  • 消息机制派发 (Message Dispatch )

    参考oc的消息调用机制
    当一个消息被派发, 运行时会顺着类的继承关系向上查找应该被调用的函数. 如果你觉得这样做效率很低, 它确实很低! 然而, 只要缓存建立了起来, 这个查找过程就会通过缓存来把性能提高到和函数表派发一样快. 但这只是消息机制的原理, 这里有一篇文章很深入的讲解了具体的技术细节.

参考文章

Swift Dispatch
[swift-evolution] Proposal: Universal dynamic dispatch for method calls
深入理解 Swift 派发机制