這次不以規(guī)律解釋行為, 而從源碼窺視規(guī)律.
在Swift中的動(dòng)與靜一文中, 我詳細(xì)的介紹了 Swift 中不同場(chǎng)景下方法的派發(fā)方式. 自認(rèn)為在這方面的掌握已經(jīng)爐火純青, Swift 的運(yùn)行機(jī)制了然于胸, 遇到問題就躍躍欲試分析一下背后的實(shí)現(xiàn)原理. 這種掌控萬物的感覺一直持續(xù)到我被一個(gè)極其簡(jiǎn)單的問題難到了為止.
一個(gè)極其簡(jiǎn)單的問題
protocol MyProtocol {
func testFunc()
}
extension MyProtocol {
func testFunc() {}
}
class MyClass: MyProtocol {
func testFunc() {}
}
這里有三個(gè)很簡(jiǎn)單的前置條件:
- 協(xié)議 MyProtocol.
- MyProtocol 的協(xié)議擴(kuò)展.
- 遵循協(xié)議的類 MyClass.
其中, 協(xié)議中聲明了 testFunc 函數(shù), 并且在擴(kuò)展中提供了 testFunc 的默認(rèn)實(shí)現(xiàn). 而 MyClass 在遵循協(xié)議的同時(shí), 自己也提供了 testFunc 的實(shí)現(xiàn).
let object: MyProtocol = MyClass()
object.testFunc()
請(qǐng)嘗試分析一下此處的方法派發(fā)方式.
這種問題當(dāng)然難不倒我, 參照Swift中的動(dòng)與靜一文, 由于 MyProtocol 的提供了對(duì) testFunc 的聲明, 因此該調(diào)用會(huì)走函數(shù)表派發(fā)方式, 具體來講, 由于 object 被聲明為 MyProtocol 類型, 最后的方法派發(fā)會(huì)通過 Existential Container 實(shí)現(xiàn)函數(shù)表派發(fā).
真正難倒我的是更簡(jiǎn)單的問題:
let object: MyClass = MyClass()
object.testFunc()
- 請(qǐng)嘗試分析一下此處的方法派發(fā)方式.
- 倘若 MyClass 沒有提供 testFunc 的默認(rèn)實(shí)現(xiàn), 是怎樣實(shí)現(xiàn)方法派發(fā)的.
問題分析
問題仿佛根本沒有問到點(diǎn)子上, 明明就是一次極其普通的函數(shù)調(diào)用, 因此, 我的直覺告訴我這應(yīng)該是直接派發(fā).
直接派發(fā)?
直接派發(fā)意味著編譯期已經(jīng)確定了函數(shù)地址, 為了證明我的猜想, 我做了一個(gè)實(shí)驗(yàn).
class MySubClass: MyClass {
override func testFunc() {}
}
let object: MyClass = MySubClass()
object.testFunc()
在編譯階段, 編譯器決議 object 的類型一定為 MyClass, 跟實(shí)際的實(shí)例變量是 MyClass() 或是 MySubClass() 沒有關(guān)系. 因此在類型一致的前提下, testFunc 調(diào)用所產(chǎn)生的行為也應(yīng)該是一致的.
然而, 該處調(diào)用的是 MySubClsss 的 testFunc 方法, 而不是 MyClass 的 testFunc. 這說明方法調(diào)用區(qū)分了 object 的類型, 而只有在運(yùn)行階段才能真正確認(rèn) object 的類型.
動(dòng)態(tài)派發(fā)?
由于各方面的文檔已經(jīng)明確表明了只有在將 object 聲明為協(xié)議類型的時(shí)候, 才會(huì)出現(xiàn) Existential Container 這種東西. 因此, 我猜想是不是沒有通過 Existential Container, 而是直接使用了 Protocol witness table(PWT) 實(shí)現(xiàn)了動(dòng)態(tài)派發(fā)?
這似乎能夠解釋我做的實(shí)驗(yàn), 在運(yùn)行期根據(jù) object 的類型在其 PWT 中找到方法對(duì)應(yīng)的實(shí)現(xiàn), 并且, 也能很好的解釋我的另一個(gè)實(shí)驗(yàn).
class MyClass: MyProtocol {}
class MySubClass: MyClass {
func testFunc() {}
}
let object: MyClass = MySubClass()
object.testFunc()
MyClass 不提供 testFunc 的實(shí)現(xiàn), 參照Swift中的動(dòng)與靜一文, MySubClass 中對(duì)于 testFunc 的實(shí)現(xiàn)也就不能注冊(cè)進(jìn) PWT 中, 因此該函數(shù)最終只會(huì)調(diào)用 MyProtocol 提供的默認(rèn)實(shí)現(xiàn).
思考
我好像得到了問題的答案, 卻感覺越來越難以理解 Swift 了, 以前信手拈來的名詞就像一個(gè)個(gè)死結(jié), 只有當(dāng)我嘗試去深挖其中的實(shí)現(xiàn)時(shí)才發(fā)現(xiàn)是一團(tuán)亂麻.
- 根據(jù)Understanding swift performance, PWT 是跟類和協(xié)議一起生成的. PWT 里面到底包含了哪些內(nèi)容?
- MyClass 遵循 MyProtocol, 未直接遵循協(xié)議的 MySubClass 究竟有沒有 PWT?
- 為什么查閱了很多資料都沒有介紹 PWT 在 ExistentialContainer 外是如何使用的?
就這樣, 一個(gè)看起來非常的簡(jiǎn)單的問題困擾了我很長(zhǎng)的一段時(shí)間, 并且翻閱了很多資料都繞過了這種場(chǎng)景的解釋.似乎是一個(gè)根本不值得分析的問題.
SIL
Swift Intermediate Language(SIL) 是 Swift 在編譯過程中的中間產(chǎn)物, 不像匯編那么難以理解, 而又足夠揭示 Swift 的運(yùn)行機(jī)制.
使用 swiftc -emit-sil
命令可以將 swift 文件編譯成 silgen(SIL 文件格式) 文件, 為了方便閱讀, 還需要使用 xcrun swift-demangle
命令將編譯后的符號(hào)還原.
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多行中間碼, 由于代碼中注釋的存在, 很容易就能提取到對(duì)應(yīng)上面兩行 Swift 代碼的中間碼.
為了方便閱讀指令, 蘋果還提供了一份非常棒的文檔 Swift Intermediate Language, 用以查閱指令.
那么閱讀就顯得簡(jiǎn)單多了, 可以看到最終對(duì)應(yīng)到 testFunc 函數(shù)調(diào)用的指令有兩條.
%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
: 該指令通過類的函數(shù)表來查找函數(shù), 基于類的實(shí)際類型. -
apply
: 傳遞參數(shù)并執(zhí)行函數(shù).
那么答案很明朗了, 采用了函數(shù)表派發(fā)的方式, 由 MyClass(或 MyClass 的子類) 執(zhí)行對(duì)應(yīng)方法, 由于我們實(shí)際類型為 MyClass, 因此最終調(diào)用的是 MyClass 的方法.
聲明為 Protocol 類型
let object: MyProtocol = MyClass()
object.testFunc()
我們已經(jīng)知道聲明為 Protocol 會(huì)使用 Existential Container 進(jìn)行動(dòng)態(tài)的方法派發(fā), 接下來看看是如何在 SIL 中體現(xiàn)的.
%0 = alloc_stack $MyProtocol, let, name "object" // users: %10, %9, %6, %1
%1 = init_existential_addr %0 : $*MyProtocol, $MyClass // user: %5
// 省略無關(guān)代碼
%6 = open_existential_addr // 省略無關(guān)代碼
%7 = witness_method // 省略無關(guān)代碼
%8 = apply // 省略無關(guān)代碼
// 省略無關(guān)代碼
對(duì)比之前的代碼, 可以發(fā)現(xiàn)在生成 object 的時(shí)候, 使用的是 init_existential_addr
指令, 該指令會(huì)生成 Existential Container 結(jié)構(gòu), 包裹著實(shí)例變量和協(xié)議對(duì)應(yīng)的 PWT.
為了找到 testFunc 的函數(shù)地址, 可以看到有兩條關(guān)鍵指令:
-
open_existential_addr
: 打開 Existential Container, 獲取包裹對(duì)象(object)的地址. -
witness_method
: 通過 PWT 獲取對(duì)應(yīng)的函數(shù)地址.
文件里同樣包含了 MyClass 所對(duì)應(yīng)的 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 提供了默認(rèn)實(shí)現(xiàn), MyClass 也提供了自己的實(shí)現(xiàn), PWT 中仍然只有一個(gè)函數(shù), @protocol witness for ClassFunc.MyProtocol.testFunc
.
在文件中同樣可以找到該函數(shù)的 SIL 實(shí)現(xiàn).
// protocol witness for MyProtocol.testFunc() in conformance MyClass
// 省略無關(guān)代碼
bb0(%0 : $*MyClass):
// 省略無關(guān)代碼
%3 = class_method %1 : $MyClass, #MyClass.testFunc!1 : (MyClass) -> () -> (), $@convention(method) (@guaranteed MyClass) -> () // user: %4
%4 = apply %3(%1) : $@convention(method) (@guaranteed MyClass) -> ()
// 省略無關(guān)代碼
}
在這個(gè)函數(shù)中有一個(gè)熟悉的指令 class_method
, 說明最終的函數(shù)地址依然是根據(jù)對(duì)象的實(shí)際類型, 通過函數(shù)表獲取的.
Protocol Witness Table
在閱讀 SIL 的過程中, PWT 的內(nèi)容是最出乎我的意料的.
當(dāng) MyProtocol 提供了 testFunc 的默認(rèn)實(shí)現(xiàn), 并且 MyClass 也提供了實(shí)現(xiàn)的情況下, MyClass 遵循該協(xié)議所生成的 PWT 卻只有孤零零的一個(gè)函數(shù), 該函數(shù)再通過函數(shù)表找到最終調(diào)用的方法.
那么, 倘若 MyClass 不提供 testFunc 的實(shí)現(xiàn)呢?
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 的內(nèi)容沒有絲毫變化, 依然只有一個(gè)孤零零的函數(shù), @protocol witness for ClassFunc.MyProtocol.testFunc
. 但是該函數(shù)的實(shí)現(xiàn)卻發(fā)生了變化.
// protocol witness for MyProtocol.testFunc() in conformance MyClass
// 省略無關(guān)代碼
bb0(%0 : $*MyClass):
// 省略無關(guān)代碼
// 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) -> ()
// 省略無關(guān)代碼
}
函數(shù)中直接調(diào)用了 MyProtocol 提供的默認(rèn) testFunc 實(shí)現(xiàn)!
回想Swift中的動(dòng)與靜一文中所舉的例子.
class MySubClass: MyClass {
func testFunc() {}
}
let object: MyProtocol = MySubClass()
object.testFunc()
之前給出的解釋是由于 MySubClass提供的實(shí)現(xiàn)沒有注冊(cè)進(jìn) PWT 導(dǎo)致無法被調(diào)用, 現(xiàn)如今又有了新的解釋:
在 MyClass 沒有提供 testFunc 的情況下, 由于沒有走函數(shù)表派發(fā), 因此 MySubClass 的實(shí)現(xiàn)是不會(huì)被調(diào)用的.
結(jié)語
閱讀文檔得到了片面的理解, 又通過閱讀源碼真正解決自己的困惑, 也算是:
紙上得來終覺淺, 絕知此事要躬行.