深入理解 Swift 的方法派發(fā)

這次不以規(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)單的前置條件:

  1. 協(xié)議 MyProtocol.
  2. MyProtocol 的協(xié)議擴(kuò)展.
  3. 遵循協(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()
  1. 請(qǐng)嘗試分析一下此處的方法派發(fā)方式.
  2. 倘若 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)用的是 MySubClssstestFunc 方法, 而不是 MyClasstestFunc. 這說明方法調(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)亂麻.

  1. 根據(jù)Understanding swift performance, PWT 是跟類和協(xié)議一起生成的. PWT 里面到底包含了哪些內(nèi)容?
  2. MyClass 遵循 MyProtocol, 未直接遵循協(xié)議的 MySubClass 究竟有沒有 PWT?
  3. 為什么查閱了很多資料都沒有介紹 PWTExistentialContainer 外是如何使用的?

就這樣, 一個(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) -> ()
  1. class_method: 該指令通過類的函數(shù)表來查找函數(shù), 基于類的實(shí)際類型.
  2. 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)鍵指令:

  1. open_existential_addr: 打開 Existential Container, 獲取包裹對(duì)象(object)的地址.
  2. 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)!

WX20180204-173449@2x

回想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é)語

閱讀文檔得到了片面的理解, 又通過閱讀源碼真正解決自己的困惑, 也算是:

紙上得來終覺淺, 絕知此事要躬行.

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末啸罢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌褂策,老刑警劉巖竟宋,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肋坚,死亡現(xiàn)場(chǎng)離奇詭異泵喘,居然都是意外死亡拉队,警方通過查閱死者的電腦和手機(jī)众眨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門握牧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娩梨,你說我怎么就攤上這事沿腰。” “怎么了狈定?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵颂龙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我纽什,道長(zhǎng)措嵌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任芦缰,我火速辦了婚禮企巢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘让蕾。我一直安慰自己浪规,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布探孝。 她就那樣靜靜地躺著笋婿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪再姑。 梳的紋絲不亂的頭發(fā)上萌抵,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音元镀,去河邊找鬼绍填。 笑死,一個(gè)胖子當(dāng)著我的面吹牛栖疑,可吹牛的內(nèi)容都是我干的讨永。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼遇革,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼卿闹!你這毒婦竟也來了揭糕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤锻霎,失蹤者是張志新(化名)和其女友劉穎著角,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旋恼,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吏口,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冰更。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片产徊。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蜀细,靈堂內(nèi)的尸體忽然破棺而出舟铜,到底是詐尸還是另有隱情,我是刑警寧澤奠衔,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布谆刨,位于F島的核電站,受9級(jí)特大地震影響归斤,放射性物質(zhì)發(fā)生泄漏痴荐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一官册、第九天 我趴在偏房一處隱蔽的房頂上張望生兆。 院中可真熱鬧,春花似錦膝宁、人聲如沸鸦难。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)合蔽。三九已至,卻和暖如春介返,著一層夾襖步出監(jiān)牢的瞬間拴事,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工圣蝎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留刃宵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓徘公,卻偏偏與公主長(zhǎng)得像牲证,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子关面,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容