介紹
首先全面了解一下褪猛,有4種派發(fā)機制楼誓,而不是兩種(靜態(tài)和動態(tài)):
- 內(nèi)聯(lián)(inline) (最快)
- 靜態(tài)派發(fā) (Static Dispatch)
- 函數(shù)表派發(fā) (Virtual Dispatch)
- 動態(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ā)有兩種形式:
- 函數(shù)表派發(fā)( Table dispatch )
- 消息派發(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
的:
這張表展示了 ParentClass 和 ChildClass 虛數(shù)表里 method1, method2, method3 在內(nèi)存里的布局.
let obj = ChildClass()
obj.method2()
當一個函數(shù)被調(diào)用時, 會經(jīng)歷下面的幾個過程:
- 讀取對象 0xB00 的函數(shù)表.
- 讀取函數(shù)指針的索引. 在這里, method2 的索引是1(偏移量), 也就是 0xB00 + 1.
- 跳到 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
, UIAppearence
和 CoreData
等功能. 這種運作方式的關(guān)鍵在于開發(fā)者可以在運行時改變函數(shù)的行為.
Objc 的函數(shù)派發(fā)都是基于消息派發(fā)的考杉。這種機制極具動態(tài)性,不止可以通過 swizzling 來改變, 甚至可以用 isa-swizzling 修改對象的繼承關(guān)系, 可以在面向?qū)ο蟮幕A(chǔ)上實現(xiàn)自定義派發(fā).
通過消息派發(fā)執(zhí)行子類中的函數(shù)的步驟:
- 到自己的方法列表中去找舰始,如果找到了崇棠,執(zhí)行對應(yīng)邏輯,如果沒找到執(zhí)行2丸卷。
- 去它的父類中去找枕稀,發(fā)現(xiàn)找到了,就執(zhí)行相應(yīng)的邏輯谜嫉。
當一個消息被派發(fā), 運行時會順著類的繼承關(guān)系向上查找應(yīng)該被調(diào)用的函數(shù). 如果你覺得這樣做效率很低, 它確實很低! 然而, 只要緩存建立了起來, 這個查找過程就會通過緩存來把性能提高到和函數(shù)表派發(fā)一樣快.
Swift 的派發(fā)機制
四個選擇具體派發(fā)方式的因素存在:
- 聲明的位置
- 引用類型
- 特定的行為
- 顯式地優(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ā)方式的表格.
這張表格展示了默認情況下 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)
可見的都會被優(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)
這張表總結(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