这次不以规律解释行为, 而从源码窥视规律.
在Swift中的动与静一文中, 我详细的介绍了 Swift 中不同场景下方法的派发方式. 自认为在这方面的掌握已经炉火纯青, Swift 的运行机制了然于胸, 遇到问题就跃跃欲试分析一下背后的实现原理. 这种掌控万物的感觉一直持续到我被一个极其简单的问题难到了为止.
一个极其简单的问题
protocol MyProtocol {
func testFunc()
}
extension MyProtocol {
func testFunc() {}
}
class MyClass: MyProtocol {
func testFunc() {}
}
这里有三个很简单的前置条件:
- 协议 MyProtocol.
- MyProtocol 的协议扩展.
- 遵循协议的类 MyClass.
其中, 协议中声明了 testFunc 函数, 并且在扩展中提供了 testFunc 的默认实现. 而 MyClass 在遵循协议的同时, 自己也提供了 testFunc 的实现.
let object: MyProtocol = MyClass()
object.testFunc()
请尝试分析一下此处的方法派发方式.
这种问题当然难不倒我, 参照Swift中的动与静一文, 由于 MyProtocol 的提供了对 testFunc 的声明, 因此该调用会走函数表派发方式, 具体来讲, 由于 object 被声明为 MyProtocol 类型, 最后的方法派发会通过 Existential Container 实现函数表派发.
真正难倒我的是更简单的问题:
let object: MyClass = MyClass()
object.testFunc()
- 请尝试分析一下此处的方法派发方式.
- 倘若 MyClass 没有提供 testFunc 的默认实现, 是怎样实现方法派发的.
问题分析
问题仿佛根本没有问到点子上, 明明就是一次极其普通的函数调用, 因此, 我的直觉告诉我这应该是直接派发.
直接派发?
直接派发意味着编译期已经确定了函数地址, 为了证明我的猜想, 我做了一个实验.
class MySubClass: MyClass {
override func testFunc() {}
}
let object: MyClass = MySubClass()
object.testFunc()
在编译阶段, 编译器决议 object 的类型一定为 MyClass, 跟实际的实例变量是 MyClass() 或是 MySubClass() 没有关系. 因此在类型一致的前提下, testFunc 调用所产生的行为也应该是一致的.
然而, 该处调用的是 MySubClsss 的 testFunc 方法, 而不是 MyClass 的 testFunc. 这说明方法调用区分了 object 的类型, 而只有在运行阶段才能真正确认 object 的类型.
动态派发?
由于各方面的文档已经明确表明了只有在将 object 声明为协议类型的时候, 才会出现 Existential Container 这种东西. 因此, 我猜想是不是没有通过 Existential Container, 而是直接使用了 Protocol witness table(PWT) 实现了动态派发?
这似乎能够解释我做的实验, 在运行期根据 object 的类型在其 PWT 中找到方法对应的实现, 并且, 也能很好的解释我的另一个实验.
class MyClass: MyProtocol {}
class MySubClass: MyClass {
func testFunc() {}
}
let object: MyClass = MySubClass()
object.testFunc()
MyClass 不提供 testFunc 的实现, 参照Swift中的动与静一文, MySubClass 中对于 testFunc 的实现也就不能注册进 PWT 中, 因此该函数最终只会调用 MyProtocol 提供的默认实现.
思考
我好像得到了问题的答案, 却感觉越来越难以理解 Swift 了, 以前信手拈来的名词就像一个个死结, 只有当我尝试去深挖其中的实现时才发现是一团乱麻.
- 根据Understanding swift performance, PWT 是跟类和协议一起生成的. PWT 里面到底包含了哪些内容?
- MyClass 遵循 MyProtocol, 未直接遵循协议的 MySubClass 究竟有没有 PWT?
- 为什么查阅了很多资料都没有介绍 PWT 在 ExistentialContainer 外是如何使用的?
就这样, 一个看起来非常的简单的问题困扰了我很长的一段时间, 并且翻阅了很多资料都绕过了这种场景的解释.似乎是一个根本不值得分析的问题.
SIL
Swift Intermediate Language(SIL) 是 Swift 在编译过程中的中间产物, 不像汇编那么难以理解, 而又足够揭示 Swift 的运行机制.
使用 swiftc -emit-sil
命令可以将 swift 文件编译成 silgen(SIL 文件格式) 文件, 为了方便阅读, 还需要使用 xcrun swift-demangle
命令将编译后的符号还原.
protocol MyProtocol {
func testFunc()
}
extension MyProtocol {
func testFunc() {}
}
class MyClass: MyProtocol {
func testFunc() {}
}
声明为 Class 类型
let object: MyClass = MyClass()
object.testFunc()
使用命令 swiftc -emit-sil ClassFunc.swift | xcrun swift-demangle > ClassFunc.silgen
获得 silgen 文件.
// function_ref MyClass.__allocating_init()
%0 = function_ref @ClassFunc.MyClass.__allocating_init() -> ClassFunc.MyClass : $@convention(method) (@thick MyClass.Type) -> @owned MyClass // user: %2
%1 = metatype $@thick MyClass.Type // user: %2
%2 = apply %0(%1) : $@convention(method) (@thick MyClass.Type) -> @owned MyClass // users: %6, %4, %5, %3
debug_value %2 : $MyClass, let, name "object" // id: %3
%4 = class_method %2 : $MyClass, #MyClass.testFunc!1 : (MyClass) -> () -> (), $@convention(method) (@guaranteed MyClass) -> () // user: %5
%5 = apply %4(%2) : $@convention(method) (@guaranteed MyClass) -> ()
strong_release %2 : $MyClass // id: %6
%7 = tuple () // user: %8
return %7 : $() // id: %8
总共生成100多行中间码, 由于代码中注释的存在, 很容易就能提取到对应上面两行 Swift 代码的中间码.
为了方便阅读指令, 苹果还提供了一份非常棒的文档 Swift Intermediate Language, 用以查阅指令.
那么阅读就显得简单多了, 可以看到最终对应到 testFunc 函数调用的指令有两条.
%4 = class_method %2 : $MyClass, #MyClass.testFunc!1 : (MyClass) -> () -> (), $@convention(method) (@guaranteed MyClass) -> () // user: %5
%5 = apply %4(%2) : $@convention(method) (@guaranteed MyClass) -> ()
-
class_method
: 该指令通过类的函数表来查找函数, 基于类的实际类型. -
apply
: 传递参数并执行函数.
那么答案很明朗了, 采用了函数表派发的方式, 由 MyClass(或 MyClass 的子类) 执行对应方法, 由于我们实际类型为 MyClass, 因此最终调用的是 MyClass 的方法.
声明为 Protocol 类型
let object: MyProtocol = MyClass()
object.testFunc()
我们已经知道声明为 Protocol 会使用 Existential Container 进行动态的方法派发, 接下来看看是如何在 SIL 中体现的.
%0 = alloc_stack $MyProtocol, let, name "object" // users: %10, %9, %6, %1
%1 = init_existential_addr %0 : $*MyProtocol, $MyClass // user: %5
// 省略无关代码
%6 = open_existential_addr // 省略无关代码
%7 = witness_method // 省略无关代码
%8 = apply // 省略无关代码
// 省略无关代码
对比之前的代码, 可以发现在生成 object 的时候, 使用的是 init_existential_addr
指令, 该指令会生成 Existential Container 结构, 包裹着实例变量和协议对应的 PWT.
为了找到 testFunc 的函数地址, 可以看到有两条关键指令:
-
open_existential_addr
: 打开 Existential Container, 获取包裹对象(object)的地址. -
witness_method
: 通过 PWT 获取对应的函数地址.
文件里同样包含了 MyClass 所对应的 PWT.
sil_witness_table hidden MyClass: MyProtocol module ClassFunc {
method #MyProtocol.testFunc!1: <Self where Self : MyProtocol> (Self) -> () -> () : @protocol witness for ClassFunc.MyProtocol.testFunc() -> () in conformance ClassFunc.MyClass : ClassFunc.MyProtocol in ClassFunc // protocol witness for MyProtocol.testFunc() in conformance MyClass
}
可以看到虽然 MyProtocol 提供了默认实现, MyClass 也提供了自己的实现, PWT 中仍然只有一个函数, @protocol witness for ClassFunc.MyProtocol.testFunc
.
在文件中同样可以找到该函数的 SIL 实现.
// protocol witness for MyProtocol.testFunc() in conformance MyClass
// 省略无关代码
bb0(%0 : $*MyClass):
// 省略无关代码
%3 = class_method %1 : $MyClass, #MyClass.testFunc!1 : (MyClass) -> () -> (), $@convention(method) (@guaranteed MyClass) -> () // user: %4
%4 = apply %3(%1) : $@convention(method) (@guaranteed MyClass) -> ()
// 省略无关代码
}
在这个函数中有一个熟悉的指令 class_method
, 说明最终的函数地址依然是根据对象的实际类型, 通过函数表获取的.
Protocol Witness Table
在阅读 SIL 的过程中, PWT 的内容是最出乎我的意料的.
当 MyProtocol 提供了 testFunc 的默认实现, 并且 MyClass 也提供了实现的情况下, MyClass 遵循该协议所生成的 PWT 却只有孤零零的一个函数, 该函数再通过函数表找到最终调用的方法.
那么, 倘若 MyClass 不提供 testFunc 的实现呢?
sil_witness_table hidden MyClass: MyProtocol module ClassFunc {
method #MyProtocol.testFunc!1: <Self where Self : MyProtocol> (Self) -> () -> () : @protocol witness for ClassFunc.MyProtocol.testFunc() -> () in conformance ClassFunc.MyClass : ClassFunc.MyProtocol in ClassFunc // protocol witness for MyProtocol.testFunc() in conformance MyClass
}
可以看到 PWT 的内容没有丝毫变化, 依然只有一个孤零零的函数, @protocol witness for ClassFunc.MyProtocol.testFunc
. 但是该函数的实现却发生了变化.
// protocol witness for MyProtocol.testFunc() in conformance MyClass
// 省略无关代码
bb0(%0 : $*MyClass):
// 省略无关代码
// function_ref MyProtocol.testFunc()
%3 = function_ref @(extension in ClassFunc):ClassFunc.MyProtocol.testFunc() -> () : $@convention(method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> () // user: %4
%4 = apply %3<MyClass>(%1) : $@convention(method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> ()
// 省略无关代码
}
函数中直接调用了 MyProtocol 提供的默认 testFunc 实现!
WX20180204-173449@2x回想Swift中的动与静一文中所举的例子.
class MySubClass: MyClass {
func testFunc() {}
}
let object: MyProtocol = MySubClass()
object.testFunc()
之前给出的解释是由于 MySubClass提供的实现没有注册进 PWT 导致无法被调用, 现如今又有了新的解释:
在 MyClass 没有提供 testFunc 的情况下, 由于没有走函数表派发, 因此 MySubClass 的实现是不会被调用的.
结语
阅读文档得到了片面的理解, 又通过阅读源码真正解决自己的困惑, 也算是:
纸上得来终觉浅, 绝知此事要躬行.
网友评论