深入理解 Swift 的方法派发

这次不以规律解释行为, 而从源码窥视规律.

Swift中的动与静一文中, 我详细的介绍了 Swift 中不同场景下方法的派发方式. 自认为在这方面的掌握已经炉火纯青, Swift 的运行机制了然于胸, 遇到问题就跃跃欲试分析一下背后的实现原理. 这种掌控万物的感觉一直持续到我被一个极其简单的问题难到了为止.

一个极其简单的问题

protocol MyProtocol {
    func testFunc()
}

extension MyProtocol {
    func testFunc() {}
}

class MyClass: MyProtocol {
    func testFunc() {}
}

这里有三个很简单的前置条件:

  1. 协议 MyProtocol.
  2. MyProtocol 的协议扩展.
  3. 遵循协议的类 MyClass.

其中, 协议中声明了 testFunc 函数, 并且在扩展中提供了 testFunc 的默认实现. 而 MyClass 在遵循协议的同时, 自己也提供了 testFunc 的实现.

let object: MyProtocol = MyClass()
object.testFunc()

请尝试分析一下此处的方法派发方式.

这种问题当然难不倒我, 参照Swift中的动与静一文, 由于 MyProtocol 的提供了对 testFunc 的声明, 因此该调用会走函数表派发方式, 具体来讲, 由于 object 被声明为 MyProtocol 类型, 最后的方法派发会通过 Existential Container 实现函数表派发.

真正难倒我的是更简单的问题:

let object: MyClass = MyClass()
object.testFunc()
  1. 请尝试分析一下此处的方法派发方式.
  2. 倘若 MyClass 没有提供 testFunc 的默认实现, 是怎样实现方法派发的.

问题分析

问题仿佛根本没有问到点子上, 明明就是一次极其普通的函数调用, 因此, 我的直觉告诉我这应该是直接派发.

直接派发?

直接派发意味着编译期已经确定了函数地址, 为了证明我的猜想, 我做了一个实验.

class MySubClass: MyClass {
    override func testFunc() {}
}

let object: MyClass = MySubClass()
object.testFunc()

在编译阶段, 编译器决议 object 的类型一定为 MyClass, 跟实际的实例变量是 MyClass() 或是 MySubClass() 没有关系. 因此在类型一致的前提下, testFunc 调用所产生的行为也应该是一致的.

然而, 该处调用的是 MySubClssstestFunc 方法, 而不是 MyClasstestFunc. 这说明方法调用区分了 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 了, 以前信手拈来的名词就像一个个死结, 只有当我尝试去深挖其中的实现时才发现是一团乱麻.

  1. 根据Understanding swift performance, PWT 是跟类和协议一起生成的. PWT 里面到底包含了哪些内容?
  2. MyClass 遵循 MyProtocol, 未直接遵循协议的 MySubClass 究竟有没有 PWT?
  3. 为什么查阅了很多资料都没有介绍 PWTExistentialContainer 外是如何使用的?

就这样, 一个看起来非常的简单的问题困扰了我很长的一段时间, 并且翻阅了很多资料都绕过了这种场景的解释.似乎是一个根本不值得分析的问题.

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) -> ()
  1. class_method: 该指令通过类的函数表来查找函数, 基于类的实际类型.
  2. 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 的函数地址, 可以看到有两条关键指令:

  1. open_existential_addr: 打开 Existential Container, 获取包裹对象(object)的地址.
  2. 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 的实现是不会被调用的.

结语

阅读文档得到了片面的理解, 又通过阅读源码真正解决自己的困惑, 也算是:

纸上得来终觉浅, 绝知此事要躬行.

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容