题外话
那天和群里一个人聊天,他去过头条京东面试,用的 Swift,问的问题我都没都听过,其中就有这个 Swift 派发机制。决定还是看下这些基础原理,然后突然想到一个问题,为什么要写博客呢,现在的想法是和别人交流的时候能逻辑很清晰的解释什么是 派发机制,不然以我的惯性,可能时间过一点就说的迷迷糊糊了。看了几篇文章,这两篇说得不错。
https://kemchenj.github.io/2016-12-25-1/。
https://zhaoxinyu.me/2018-04-08-method-dispatch-in-swift-1/
https://www.jianshu.com/p/cfe7da01880d
下面文章写的有点怪,是先带着结果看问题的。因为这也是我最开始的思路,和其他博客不太一样,我并不是先遇到问题。这个话题是我想了解所以就直接查的问题,得出结论,然后根据结论分析一些代码例子,最后一步是提供验证这些结论是否正确的方法,因为最后一步比较乏味。
派发机制
通过阅读上面两篇文章和 SIL 代码验证过后,swift 有四种派发方式:
- static dispatch:静态派发(直接派发)
- table dispatch:函数表派发, 通过 SIL 分析,swift 中有两种函数表,协议用的 witness_table,类用的 virtual_table
- message dispatch:消息派发,OC 中常用的派发方式(第一篇文章中说基于这种派发,可以运行时改变函数行为, 这里不太明白,最下面问题2)
第一篇文章中关于这个部分说的比较详细,大段抄过来可能涉及侵权。。
总结
派发机制总结表(我没验证全,用 SIL 验证了部分,剩余是看上面的文章的)
- 表里左边的即是声明引用类型,非实际对象类型;
- 根据 SIL 在非编译器优化的情况下得出的结论,有的情况下会把 table 派发优化成直接派发,不在讨论情况中
图中那个问好是我实践的和其他文章说的不一样,最下面有说
除上图外的重要点:
- 引用的类型决定了派发的方式。
- NSObjectClass 和 Class 没什么区别(至少编译器未优化前是这样)
根据结论用代码看问题
根据以上逻辑,有两个代码问题,刚看的时候有点疑惑,现在可以用上面的东西解释下
- 先看一个简单的逻辑
protocol MyProtocol {
func testFuncA()
}
extension MyProtocol {
func testFuncA() {
print("protocl - funcA")
}
}
class Myclass: MyProtocol {
func testFuncA() {
print("class - funcA")
}
}
let x: MyProtocol = Myclass()
x.testFuncA() // class - funcA
由于协议函数表里声明了 funcA,所以用协议函数表查找实现,找到了在 Myclass 中的实现,走的函数表派发。
- 再看下下面这段代码
protocol MyProtocol {
}
extension MyProtocol {
func testFuncA() {
print("protocl - funcA")
}
}
class Myclass: MyProtocol {
func testFuncA() {
print("class - funcA")
}
}
let x: MyProtocol = Myclass()
x.testFuncA() // protocl - funcA
调用协议的方法是因为派发方式由引用类型决定,而协议里没有 funcA 的声明,所以协议函数表里就不存在这个方法,也不会去根据表查找实现,就走了协议的 extension 的直接派发。
第三个问题 是上述文章提到的 SR-103
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi() {
print("Hello")
}
}
class Person: Greetable {}
class LoudPerson: Person {
func sayHi() {
print("HELLO")
}
}
let x: Greetable = LoudPerson()
x.sayHi() // Hello,protocol
这个被确定是一个bug。按照正常的理解,协议函数表会查找子类的 sayhi 实现,但是实际上只找了遵循协议的那个 person 类,找不到,所以就走了协议 extension 的直接派发。但是三年过去了,这个bug好像并没被修复。。
SIL 验证过程
Swift Intermediate Language(SIL) 是 Swift 在编译过程中的中间产物。现在把这个文件解析成 SIL,上面第二个文章也说了方法,这里补充说明下吧。
swiftc -emit-silgen xxx.swift -Onone > xxxx.sil
struct TestStruct {
func testFunc1() { print("ss") }
}
let t = TestStruct()
t.testFunc1()
如下图所示,我们要分析的是 main 中的代码,通过搜寻函数名关键字可以看到 %9 那里是 function_ref
,可以看出这个是直接派发
如何看是否是派发方式:
-
function_ref
:直接派发 -
class_method
:虚函数表派发 -
witness_method
:证据表派发(不知道咋翻译了) -
objc_method
:消息机制派发
问题:
- 两个文章都说 subclass.extension 会采用 消息机制派发,但是我实验不出来这种情况,之前他们有个例子是用在 extension 中 override ,但是现在这种写法编译器报错了,不知道是不是因为这个
- 为什么函数表的方式无法动态生成函数插入到表里,消息派发机制可以,消息派发机制如何动态添加函数表的,暂时猜测runtime的函数表是多个表的集合,涉及到了 runtime 原理,下次看看这个原理,如果内容可以,就再记录下