Swift-函数派发
Swift有三种函数派发方式,总结一下
- 静态派发
- 函数表派发
- 消息派发
派发方式概述
静态派发
静态派发
是三种派发方式中最快的。CPU
直接拿到函数地址并进行调用。编译器优化时,也常常将函数进行内联,将其转换为静态派发方式,提升执行速度。C++
默认使用静态派发;在Swift
中给函数加上final
关键字,也会变成静态派发
。
优点:
使用最少的指令集,办最快的事情。
缺点:
静态派发最大的弊病就是没有动态性,不支持继承。
函数表派发
编译型语言中最常见的派发方式,既保证了动态性也兼顾了执行效率。
函数所在的类会维护一个“函数表”(虚函数表),存取了每个函数实现的指针。
每个类的 V-Table 在编译时就会被构建,所以与静态派发相比多出了两个读取的工作:
- 读取该类的 V-Table
- 读取函数的指针
优点:
- 查表是一种简单,易实现,而且性能可预知的方式。
- 理论上说,函数表派发也是一种高效的方式。
缺点:
- 与静态派发相比,从字节码角度来看,多了两次读和一次跳转。
- 与静态派发相比,编译器对某些含有副作用的函数无法优化。(过度拆解,无法内联)
Swift
类扩展里面的函数无法动态加入该类的函数表中,只能使用静态派发的方式。
消息派发
消息机制是调用函数最动态的方式。由于Swfit
使用的依旧是Objective-C
的运行时系统,消息派发其实也就是Objective-C
的Message Passing(消息传递)
。由于消息传递大家看的文章很多了,这里不做过多赘述。
1 | id returnValue = [obj 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
失效。因为,这个属性的getter
和setter
会被优化为静态派发。虽然,代码可以通过编译,不过动态生成的KVO
函数就不会被触发。