Swift-函数派发

Swift有三种函数派发方式,总结一下

  • 静态派发
  • 函数表派发
  • 消息派发

派发方式概述

静态派发

静态派发是三种派发方式中最快的。CPU直接拿到函数地址并进行调用。编译器优化时,也常常将函数进行内联,将其转换为静态派发方式,提升执行速度。
C++默认使用静态派发;在Swift中给函数加上final关键字,也会变成静态派发

优点:
使用最少的指令集,办最快的事情。

缺点:
静态派发最大的弊病就是没有动态性,不支持继承。

函数表派发

编译型语言中最常见的派发方式,既保证了动态性也兼顾了执行效率。
函数所在的类会维护一个“函数表”(虚函数表),存取了每个函数实现的指针。
每个类的 V-Table 在编译时就会被构建,所以与静态派发相比多出了两个读取的工作:

  • 读取该类的 V-Table
  • 读取函数的指针

优点:

  • 查表是一种简单,易实现,而且性能可预知的方式。
  • 理论上说,函数表派发也是一种高效的方式。

缺点:

  • 与静态派发相比,从字节码角度来看,多了两次读和一次跳转。
  • 与静态派发相比,编译器对某些含有副作用的函数无法优化。(过度拆解,无法内联)
  • Swift类扩展里面的函数无法动态加入该类的函数表中,只能使用静态派发的方式。

消息派发

消息机制是调用函数最动态的方式。由于Swfit使用的依旧是Objective-C的运行时系统,消息派发其实也就是Objective-CMessage Passing(消息传递)。由于消息传递大家看的文章很多了,这里不做过多赘述。

1
2
3
id returnValue = [obj messageName:param];
// 底层代码
id returnValue = objc_msgSend(obj, @selector(messageName:), param);

优点:

  • 动态性高
  • Method Swizzling
  • isa Swizzling

缺点:

  • 执行效率是三种派发方式中最低的

所幸的是objc_msgSend会将匹配的结果缓存到一个映射表中,每个类都有这样一块缓存。若是之后发送相同的消息,执行速率会很快。

Swift的派发机制

Swift的派发机制受到4个因素的影响:

  • 数据类型
  • 函数声明的位置
  • 指定派发方式
  • 编译器优化

数据类型 && 声明位置

类型\位置 初始声明 扩展
值类型 静态派发 静态派发
协议 函数表派发 静态派发
函数表派发 静态派发
NSObject子类 函数表派发 静态派发

声明在 协议 或者 类 中的函数是使用函数表派发的
声明在 扩展 中的函数则是静态派发

指定派发方式

给函数添加关键字的修饰也会改变其派发方式。

final

添加了final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可见。

dynamic

函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。

@objc

该关键字可以将Swift函数暴露给Objc运行时,但并不会改变其派发方式,依旧是函数表派发。

@objc + dynamic

这两个关键字组合使用,将采用消息派发的方式

@inline

告诉编译器将此函数静态派发,但将其转换成SIL代码后,依旧是函数表派发

static

static 关键字会将函数变为静态派发,不会在 vtable 中出现。

类型 静态派发 函数表派发 消息派发
值类型 所有方法 - -
协议 extension 主体创建 -
extension/final/static 主体创建 @objc + dynamic
NSObject子类 extension/final/static 主体创建 @objc + dynamic

除此之外,编译器可能将某些方法优化为静态派发。例如,私有函数。

编译器优化

Swift会尽可能的去优化函数派发方式。当一个类声明了一个私有函数时,该函数很可能会被优化为静态派发。
这也就是为什么在Swift中使用target-action模式时,私有selector会报错的原因(Objective-C无法获取#selector指定的函数)。

另一个需要注意的是,NSObject子类中的属性,如果没有使用dynamic修饰的话,这个优化会默认让KVO失效。因为,这个属性的gettersetter会被优化为静态派发。虽然,代码可以通过编译,不过动态生成的KVO函数就不会被触发。