Swift 的函數(shù)派發(fā)機制

介紹

首先全面了解一下褪猛,有4種派發(fā)機制楼誓,而不是兩種(靜態(tài)和動態(tài)):

  1. 內(nèi)聯(lián)(inline) (最快)
  2. 靜態(tài)派發(fā) (Static Dispatch)
  3. 函數(shù)表派發(fā) (Virtual Dispatch)
  4. 動態(tài)派發(fā) (Dynamic Dispatch)(最慢)

函數(shù)派發(fā)就是程序判斷使用哪種途徑去調(diào)用一個函數(shù)的機制. 每次函數(shù)被調(diào)用時都會被觸發(fā), 但你又不會太留意的一個東西. 了解派發(fā)機制對于寫出高性能的代碼來說很有必要, 而且也能夠解釋很多 Swift 里"奇怪"的行為.

注:編譯型語言有三種基礎(chǔ)的函數(shù)派發(fā)方式:

  • 直接派發(fā)(Direct Dispatch)
  • 函數(shù)表派發(fā)(Table Dispatch)
  • 消息機制派發(fā)(Message Dispatch)

大多數(shù)語言都會支持一到兩種, Java 默認使用函數(shù)表派發(fā), 但你可以通過 final 修飾符修改成直接派發(fā). C++ 默認使用直接派發(fā), 但可以通過加上 virtual 修飾符來改成函數(shù)表派發(fā). 而 Objective-C 則總是使用消息機制派發(fā), 但允許開發(fā)者使用 C 直接派發(fā)來獲取性能的提高. 這樣的方式非常好, 但也給很多開發(fā)者帶來了困擾于微。

派發(fā)方式 (Types of Dispatch )

一個方法會在運行時被喚起調(diào)用,是因為編譯器有一個計算機制,用來選擇正確的方法,然后通過傳遞參數(shù)來喚起它,這個機制通常被成為 派發(fā)(dispatch)等脂。

直接派發(fā) (Direct Dispatch)

直接派發(fā)裁奇,又叫 靜態(tài)派發(fā)(static dispatch) 是在編譯期就完全確定調(diào)用方法的分派方式.

用于在多態(tài)情況下,在編譯期就實現(xiàn)對于確定的類型,在函數(shù)調(diào)用表中推斷和追溯正確的方法,包括列舉泛型的特定版本,在提供的全部函數(shù)定義中選擇的特定實現(xiàn).

在編譯器確定使用 static dispatch 后桐猬,會在生成的可執(zhí)行文件內(nèi),直接指定包含了方法實現(xiàn)內(nèi)存地址的指針刽肠,編譯器直接找到相關(guān)指令的位置溃肪。當函數(shù)調(diào)用時,系統(tǒng)直接跳轉(zhuǎn)到函數(shù)的內(nèi)存地址執(zhí)行操作音五。

直接派發(fā)是非潮棺快的,調(diào)用指令少躺涝,執(zhí)行快厨钻,編譯器可以在編譯期定位到函數(shù)的位置。因此,當函數(shù)被調(diào)用時莉撇,編譯器能通過函數(shù)的內(nèi)存地址呢蛤,直接找到它的函數(shù)實現(xiàn)。這極大的提高了性能棍郎,可以到達類似inline的編譯期優(yōu)化其障。同時允許編譯器能夠執(zhí)行例如內(nèi)聯(lián)等優(yōu)化,缺點是由于缺少動態(tài)性而不支持繼承涂佃。

事實上励翼,編譯期在編譯階段為了能夠獲取最大的性能提升,都盡量將函數(shù)靜態(tài)化辜荠。

動態(tài)派發(fā)

如前所述, 在這種類型的派發(fā)中汽抚,在運行時而不是編譯時選擇實現(xiàn)方法,這會增加一下性能開銷伯病。

這里也許你會有這樣的疑問造烁?既然動態(tài)派發(fā)有性能開銷,我們?yōu)槭裁催€要使用它午笛?

因為它具有靈活性惭蟋。實際上,大多數(shù)的OOP語言都支持動態(tài)派發(fā)药磺,因為它允許多態(tài)告组。

動態(tài)派發(fā)有兩種形式:

  1. 函數(shù)表派發(fā)( Table dispatch )
  2. 消息派發(fā)( Message dispatch )
函數(shù)表派發(fā) (Table Dispatch )

函數(shù)表派發(fā)是編譯型語言實現(xiàn)動態(tài)行為最常見的實現(xiàn)方式。
函數(shù)表使用了一個數(shù)組來存儲類聲明的每一個函數(shù)的指針癌佩。大部分語言把這個稱為 “virtual table”(虛函數(shù)表)木缝,Swift 里稱為 “witness table”。每一個類都會維護一個函數(shù)表围辙,里面記錄著類所有的函數(shù)我碟,如果父類函數(shù)被override,表里面只會保存被 override 之后的函數(shù)姚建。一個子類新添加的函數(shù)怎囚,都會被插入到這個數(shù)組的最后。運行時會根據(jù)這一個表去決定實際要被調(diào)用的函數(shù)桥胞。

舉個例子, 看看下面兩個類:

class ParentClass {
    func method1() {}
    func method2() {}
}
class ChildClass: ParentClass {
    override func method2() {}
    func method3() {}
}

在這個情況下, 編譯器會創(chuàng)建兩個函數(shù)表, 一個是 ParentClass 的, 另一個是 ChildClass的:

image.png

這張表展示了 ParentClass 和 ChildClass 虛數(shù)表里 method1, method2, method3 在內(nèi)存里的布局.

let obj = ChildClass()
obj.method2()

當一個函數(shù)被調(diào)用時, 會經(jīng)歷下面的幾個過程:

  1. 讀取對象 0xB00 的函數(shù)表.
  2. 讀取函數(shù)指針的索引. 在這里, method2 的索引是1(偏移量), 也就是 0xB00 + 1.
  3. 跳到 0x222 (函數(shù)指針指向 0x222)

一個函數(shù)被調(diào)用時會先去讀取對象的函數(shù)表恳守,再根據(jù)類的地址加上該的函數(shù)的偏移量得到函數(shù)地址,最后跳到那個地址上去贩虾。從編譯后的字節(jié)碼這方面來看就是 兩次讀取一次跳轉(zhuǎn)催烘, 由此帶來了性能的損耗. 另一個慢的原因在于編譯器可能會由于函數(shù)內(nèi)執(zhí)行的任務(wù)導(dǎo)致無法優(yōu)化,比直接派發(fā)還是慢了些缎罢,但仍比消息分派快伊群。

這種基于數(shù)組的實現(xiàn), 缺陷在于函數(shù)表無法拓展. 子類會在虛數(shù)函數(shù)表的最后插入新的函數(shù), 沒有位置可以讓 extension 安全地插入函數(shù).

消息機制派發(fā) (Message Dispatch )

消息機制是調(diào)用函數(shù)最動態(tài)的方式. 也是 Cocoa 的基石, 這樣的機制催生了 KVO, UIAppearenceCoreData 等功能. 這種運作方式的關(guān)鍵在于開發(fā)者可以在運行時改變函數(shù)的行為.

Objc 的函數(shù)派發(fā)都是基于消息派發(fā)的考杉。這種機制極具動態(tài)性,不止可以通過 swizzling 來改變, 甚至可以用 isa-swizzling 修改對象的繼承關(guān)系, 可以在面向?qū)ο蟮幕A(chǔ)上實現(xiàn)自定義派發(fā).

image.png

通過消息派發(fā)執(zhí)行子類中的函數(shù)的步驟:

  1. 到自己的方法列表中去找舰始,如果找到了崇棠,執(zhí)行對應(yīng)邏輯,如果沒找到執(zhí)行2丸卷。
  2. 去它的父類中去找枕稀,發(fā)現(xiàn)找到了,就執(zhí)行相應(yīng)的邏輯谜嫉。

當一個消息被派發(fā), 運行時會順著類的繼承關(guān)系向上查找應(yīng)該被調(diào)用的函數(shù). 如果你覺得這樣做效率很低, 它確實很低! 然而, 只要緩存建立了起來, 這個查找過程就會通過緩存來把性能提高到和函數(shù)表派發(fā)一樣快.

Swift 的派發(fā)機制

四個選擇具體派發(fā)方式的因素存在:

  1. 聲明的位置
  2. 引用類型
  3. 特定的行為
  4. 顯式地優(yōu)化 (Visibility Optimizations)

Swift 沒有在文檔里具體寫明什么時候會使用函數(shù)表什么時候使用消息機制. 唯一的承諾是使用 dynamic 修飾的時候會通過 Objective-C 的運行時進行消息機制派發(fā). 下面都只是 Swift 3.0 里的結(jié)果, 并且很可能在之后的版本更新里進行修改.

聲明的位置 (Location Matters)

在 Swift 里, 一個函數(shù)有兩個可以聲明的位置: 類型聲明的作用域extension. 根據(jù)聲明類型的不同, 也會有不同的派發(fā)方式.

class MyClass {
    func mainMethod() {}
}
extension MyClass {
    func extensionMethod() {}
}

上面的例子里, mainMethod 會使用函數(shù)表派發(fā), 而 extensionMethod 則會使用直接派發(fā). 直覺上這兩個函數(shù)的聲明方式并沒有那么大的差異. 下面是根據(jù)類型, 聲明位置總結(jié)出來的函數(shù)派發(fā)方式的表格.

image.png

這張表格展示了默認情況下 Swift 使用的派發(fā)方式.

總結(jié)起來有這么幾點:

  • struct 和 enum 都是值類型, 不支持繼承萎坷,編譯器將他們置為靜態(tài)派發(fā)下,因為他們永遠不可能被子類化沐兰,所以值類型總是會使用直接派發(fā)
  • 而協(xié)議和類的 extension 都會使用直接派發(fā)
  • NSObject 的 extension 會使用消息機制進行派發(fā)
  • NSObject 聲明作用域里的函數(shù)都會使用函數(shù)表進行派發(fā).
  • 協(xié)議里聲明的, 并且?guī)в心J實現(xiàn)的函數(shù)會使用函數(shù)表進行派發(fā)

引用類型 (Reference Type Matters)

指定派發(fā)方式 (Specifying Dispatch Behavior)

Swift 有一些修飾符可以指定派發(fā)方式.

final

final 允許類里面的函數(shù)使用直接派發(fā). 這個修飾符會讓函數(shù)失去動態(tài)性. 任何函數(shù)都可以使用這個修飾符, 就算是 extension 里本來就是直接派發(fā)的函數(shù). 這也會讓 Objective-C 的運行時獲取不到這個函數(shù), 不會生成相應(yīng)的 selector.

dynamic

dynamic 可以讓類里面的函數(shù)使用消息機制派發(fā). 使用 dynamic, 必須導(dǎo)入 Foundation 框架, 里面包括了 NSObject 和 Objective-C 的運行時. dynamic 可以讓聲明在 extension 里面的函數(shù)能夠被 override. dynamic 可以用在所有 NSObject 的子類和 Swift 的原聲類.

@objc & @nonobjc

@objc@nonobjc 顯式地聲明了一個函數(shù)是否能被 Objective-C 的運行時捕獲到. 使用 @objc 的典型例子就是給 selector 一個命名空間 @objc(abc_methodName), 讓這個函數(shù)可以被 Objective-C 的運行時調(diào)用. @nonobjc 會改變派發(fā)的方式, 可以用來禁止消息機制派發(fā)這個函數(shù), 不讓這個函數(shù)注冊到 Objective-C 的運行時里. 我不確定這跟 final 有什么區(qū)別, 因為從使用場景來說也幾乎一樣. 我個人來說更喜歡 final, 因為意圖更加明顯.

譯者注: 我個人感覺, 這這主要是為了跟 Objective-C 兼容用的, final 等原生關(guān)鍵詞, 是讓 Swift 寫服務(wù)端之類的代碼的時候可以有原生的關(guān)鍵詞可以使用

final @objc

可以在標記為 final 的同時, 也使用 @objc 來讓函數(shù)可以使用消息機制派發(fā). 這么做的結(jié)果就是, 調(diào)用函數(shù)的時候會使用直接派發(fā), 但也會在 Objective-C 的運行時里注冊響應(yīng)的 selector. 函數(shù)可以響應(yīng) perform(selector:) 以及別的 Objective-C 特性, 但在直接調(diào)用時又可以有直接派發(fā)的性能.

@inline

Swift 也支持 @inline, 告訴編譯器可以使用直接派發(fā).

修飾符總結(jié) (Modifier Overview)

image.png

可見的都會被優(yōu)化 (Visibility Will Optimize)

Swift 會盡最大能力去優(yōu)化函數(shù)派發(fā)的方式. 例如, 如果你有一個函數(shù)從來沒有 override, Swift 就會檢車并且在可能的情況下使用直接派發(fā). 這個優(yōu)化大多數(shù)情況下都表現(xiàn)得很好, 但對于使用了 target / action 模式的 Cocoa 開發(fā)者就不那么友好了. 例如:

override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.rightBarButtonItem = UIBarButtonItem(
        title: "登錄", style: .plain, target: nil,
        action: #selector(ViewController.signInAction)
    )
}
private func signInAction() {}

這里編譯器會拋出一個錯誤:

Argument of '#selector' refers to instance method 'btnDidClick(sender:)' that is not exposed to Objective-C

Add '@objc' to expose this instance method to Objective-C

Objective-C 無法獲取 #selector 指定的函數(shù). Swift 會把這個函數(shù)優(yōu)化為直接派發(fā)的話, 就能理解這件事情了. 這里修復(fù)的方式很簡單:
要使用動態(tài)性哆档,我們需要使用 dynamic 關(guān)鍵字。Swift4.0之前住闯,我們需要一起使用 dynamic@objc. Swift4.0之后瓜浸,我們需要表明 @objc 讓我們的方法支持Objective-C的調(diào)用,以支持消息派發(fā)比原。

派發(fā)總結(jié) (Dispatch Summary)

image.png
image.png

這張表總結(jié)引用類型, 修飾符和它們對于 Swift 函數(shù)派發(fā)的影響

現(xiàn)在如何證明這些方法是哪種派發(fā)技術(shù)插佛?

為此,我們必須看一下Swift中間語言(SIL)春寿。通過在網(wǎng)上可以進行的研究朗涩,發(fā)現(xiàn)有一種方法:

1. 如果函數(shù)使用Table派發(fā)忽孽,則它會出現(xiàn)在vtable(或witness_table)中

sil_vtable Animal { 
#Animal.isCute绑改!1:(Animal)->()->():main.Animal.isCute()->()// Animal.isCute()
…… 
}

2. 如果函數(shù)使用 Message Dispatch,則關(guān)鍵字volatile應(yīng)該存在于調(diào)用中兄一。另外厘线,您將找到兩個標記foreign和objc_method,指示使用Objective-C運行時調(diào)用了該函數(shù)出革。

%14 = class_method [volatile]%13:$ Dog造壮,#Dog.goWild!1.foreign:(Dog)->()->()骂束,$ @ convention(objc_method)(Dog)->() 

3. 如果沒有以上兩種情況的證據(jù)耳璧,答案是靜態(tài)派發(fā)。

參考文章

深入理解 Swift 派發(fā)機制
Swift的靜態(tài)派發(fā)和動態(tài)派發(fā)機制
Static Dispatch Over Dynamic Dispatch

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末展箱,一起剝皮案震驚了整個濱河市旨枯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌混驰,老刑警劉巖攀隔,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皂贩,死亡現(xiàn)場離奇詭異,居然都是意外死亡昆汹,警方通過查閱死者的電腦和手機明刷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來满粗,“玉大人辈末,你說我怎么就攤上這事“芰剩” “怎么了本冲?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長劫扒。 經(jīng)常有香客問我檬洞,道長,這世上最難降的妖魔是什么沟饥? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任添怔,我火速辦了婚禮,結(jié)果婚禮上贤旷,老公的妹妹穿的比我還像新娘广料。我一直安慰自己,他們只是感情好幼驶,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布艾杏。 她就那樣靜靜地躺著,像睡著了一般盅藻。 火紅的嫁衣襯著肌膚如雪购桑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天氏淑,我揣著相機與錄音勃蜘,去河邊找鬼。 笑死假残,一個胖子當著我的面吹牛缭贡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辉懒,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼阳惹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了眶俩?” 一聲冷哼從身側(cè)響起莹汤,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仿便,沒想到半個月后体啰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攒巍,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年荒勇,在試婚紗的時候發(fā)現(xiàn)自己被綠了柒莉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡沽翔,死狀恐怖兢孝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仅偎,我是刑警寧澤跨蟹,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站橘沥,受9級特大地震影響窗轩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜座咆,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一痢艺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧介陶,春花似錦堤舒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至某残,卻和暖如春国撵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驾锰。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工卸留, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留走越,地道東北人椭豫。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像旨指,于是被迫代替她去往敵國和親赏酥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

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