原文地址:Static vs Dynamic Dispatch in Swift: A decisive choicegit
參考文獻:Method dispatch in Swiftgithub
若是你瞭解面向對象,對於 方法派發機制
應該不陌生。swift
首先說下第一個結論:靜態派發機制
同時支持 值類型
和 引用類型
。數組
然而,動態派發機制僅支持 引用類型(reference types)
, 好比 Class
。簡而言之: 對於動態性或者動態派發,咱們須要用到繼承特性,而這是值類型不支持的。緩存
牢記這一點,咱們接着往下看!app
首先全面瞭解一下,由4種派發機制,而不是兩種(靜態和動態):由編譯器決定應該使用哪一種派發技術。固然,優先選擇內聯函數, 而後按需選擇。框架
Objective-C默認支持動態派發
, 這種派發技術以多態的形式爲開發人員提供了靈活性。好比子類能夠重寫父類的方法,這很棒,然而,這也是須要代價的。less
動態派發以必定量的運行時開銷爲代價,提升了語言的靈活性。這意味着,在動態派發機制下,對於每一個方法的調用,編譯器必須在方法列表(witness table(虛函數表或者其餘語言中的動態表))中查找執行方法的實現。編譯器須要判斷調用方,是選擇父類的實現,仍是子類的實現。並且因爲全部對象的內存都是在運行時分配的,所以編譯器只能在運行時執行檢查。函數
而靜態調用,則沒有這個問題。在編譯期的時候,編譯器就知道要爲某個方法調用某種實現。所以, 編譯器能夠執行某些優化,甚至在可能的狀況下,能夠將某些代碼轉換成inline函數,從而使總體執行速度異常快。性能
如何在Swift中實現動態派發和靜態派發?
要實現動態派發,咱們可使用繼承,重寫父類的方法。另外咱們可使用dynamic關鍵字,而且須要在*@objc*關鍵字前面加上關鍵字,以便將方法公開給OC runtime使用。
要實現靜態派發,咱們可使用final和static關鍵字,保證不會被覆寫。
注: 編譯性語言有3種基礎的函數派發方式: 直接派發(Direct Dispatch),函數表派發(Table Dispatch), 消息機制派發(Message Dispatch)
如上面所說,他們和動態派發
相比,很是快。編譯器能夠在編譯期定位到函數的位置。所以,當函數被調用時,編譯器能經過函數的內存地址,直接找到它的函數實現。這極大的提升了性能,能夠到達相似inline的編譯期優化。
如前所述, 在這種類型的派發中,在運行時而不是編譯時選擇實現方法,這會增長一下性能開銷。
由於它具備靈活性。實際上,大多數的OOP語言都支持動態派發,由於它容許多態。這裏也許你會有這樣的疑問?既然動態派發有性能開銷,咱們爲何還要使用它?
動態派發有兩種形式:
這種調用方式利用一個表,該表是一組函數指針,稱爲witness table,以查找特性方法的實現。
witness table如何工做?
因爲編譯器必須從表中讀取方法實現的內存地址,而後跳轉到該地址,所以它須要兩條附加指令,所以它比靜態分派慢,但仍比消息分派快。
注意:我不太肯定,可是這種特殊的派發技術能夠是虛擬派發,由於它利用了虛擬表,可是我找不到具體的參考。
這種動態派發方式是最動態的。事實上,它表現優異(省去了優化部分),目前,Cocoa框架在KVO,CoreData等不少地方在使用它。
此外,它還可使用method swizzling
, 咱們能夠在運行時更改函數的實現。
eg:
let original = #selector(getter: UIViewController.childForStatusBarStyle)
let swizzled = #selector(getter: UIViewController.swizzledChildForStatusBarStyle)
let originalMethod = class_getInstanceMethod(UIViewController.self, original)
let swizzled = class_getInstanceMethod(UIViewController.self, swizzled)
method_exchangeImplementations(originalMethod, swizzledMethod)
複製代碼
目前,Swift自己不支持這種功能,而是利用Objective-C的runtime特性,間接實現這種動態性。
要使用動態性,咱們須要使用dynamic
關鍵字。在Swift4.0以前,咱們須要一塊兒使用dynamic
和@objc
. Swift4.0以後,咱們須要代表@objc
讓咱們的方法支持Objective-C的調用,以支持消息派發。
因爲咱們使用了Objective-C的runtime特性, 當一個message被髮送時, runtime會去動態查找方法的實現(implemention)。這很慢,爲了提供效率,咱們使用緩存來儘量的讓經常使用的方法被快速找到。
struct Person {
func isIrritating() -> Bool { } // Static
}
extension Person {
func canBeEasilyPissedOff() -> Bool { } // Static
}
複製代碼
因爲struct
和enum
都是值類型
, 不支持繼承,編譯器將他們置爲靜態派發下,由於他們永遠不可能被子類化。
Protocol Animal {
func isCute() -> Bool { } // Table
}
extension Animal {
func canGetAngry() -> Bool { } // Static
}
複製代碼
這裏的重點是在extenison(擴展)裏面定義的函數,使用靜態派發(static dispatch)
class Dog: Animal {
func isCute() -> Bool { } // Tablel
@objc dynamic func hoursSleep() -> Int { } // Message
}
extenison Dog {
func canBite() -> Bool { } // Static
@objc func goWild() { } // Message
}
final class Employee {
func canCode() -> Bool { } // Static
}
複製代碼
@objc
,使用靜態派發(static dispatch)好吧,如今這只是我在講,您相信我所說的一切,對嗎?
如今如何證實這些方法其實是使用我上面解釋的派發技術?
爲此,咱們必須看一下Swift中間語言(SIL)。經過我在網上能夠進行的研究,我發現有一種方法:
vtable
(或witness_table
)中sil_vtable Animal {
#Animal.isCute!1:(Animal)->()->():main.Animal.isCute()->()// Animal.isCute()
……
}
複製代碼
volatile
應該存在於調用中。另外,您將找到兩個標記foreign
和objc_method
,指示使用Objective-C運行時調用了該函數。%14 = class_method [volatile]%13:$ Dog,#Dog.goWild!1.foreign:(Dog)->()->(),$ @ convention(objc_method)(Dog)->()
複製代碼
靜態派發
。