[翻譯]Swift的靜態派發和動態派發機制

原文地址:Static vs Dynamic Dispatch in Swift: A decisive choicegit

參考文獻:Method dispatch in Swiftgithub

若是你瞭解面向對象,對於 方法派發機制 應該不陌生。swift

基礎知識

首先說下第一個結論:靜態派發機制 同時支持 值類型引用類型數組

然而,動態派發機制僅支持 引用類型(reference types), 好比 Class 。簡而言之: 對於動態性或者動態派發,咱們須要用到繼承特性,而這是值類型不支持的。緩存

牢記這一點,咱們接着往下看!app

首先全面瞭解一下,由4種派發機制,而不是兩種(靜態和動態):

  1. 內聯(inline) (最快)
  2. 靜態派發 (Static Dispatch)
  3. 函數表派發 (Virtual Dispatch)
  4. 動態派發 (Dynamic Dispatch)(最慢)

由編譯器決定應該使用哪一種派發技術。固然,優先選擇內聯函數, 而後按需選擇。框架

靜態派發 vs 動態派發 或者 Swift vs Objective-C

Objective-C默認支持動態派發, 這種派發技術以多態的形式爲開發人員提供了靈活性。好比子類能夠重寫父類的方法,這很棒,然而,這也是須要代價的。less

動態派發以必定量的運行時開銷爲代價,提升了語言的靈活性。這意味着,在動態派發機制下,對於每一個方法的調用,編譯器必須在方法列表(witness table(虛函數表或者其餘語言中的動態表))中查找執行方法的實現。編譯器須要判斷調用方,是選擇父類的實現,仍是子類的實現。並且因爲全部對象的內存都是在運行時分配的,所以編譯器只能在運行時執行檢查。函數

而靜態調用,則沒有這個問題。在編譯期的時候,編譯器就知道要爲某個方法調用某種實現。所以, 編譯器能夠執行某些優化,甚至在可能的狀況下,能夠將某些代碼轉換成inline函數,從而使總體執行速度異常快。性能

如何在Swift中實現動態派發和靜態派發?

  1. 要實現動態派發,咱們可使用繼承,重寫父類的方法。另外咱們可使用dynamic關鍵字,而且須要在*@objc*關鍵字前面加上關鍵字,以便將方法公開給OC runtime使用。

  2. 要實現靜態派發,咱們可使用finalstatic關鍵字,保證不會被覆寫。

讓咱們繼續深刻下去:

注: 編譯性語言有3種基礎的函數派發方式: 直接派發(Direct Dispatch),函數表派發(Table Dispatch), 消息機制派發(Message Dispatch)

靜態派發(或者直接派發)

如上面所說,他們和動態派發相比,很是快。編譯器能夠在編譯期定位到函數的位置。所以,當函數被調用時,編譯器能經過函數的內存地址,直接找到它的函數實現。這極大的提升了性能,能夠到達相似inline的編譯期優化。

動態派發

如前所述, 在這種類型的派發中,在運行時而不是編譯時選擇實現方法,這會增長一下性能開銷。

這裏也許你會有這樣的疑問?既然動態派發有性能開銷,咱們爲何還要使用它?

由於它具備靈活性。實際上,大多數的OOP語言都支持動態派發,由於它容許多態。

動態派發有兩種形式:

  1. 函數表派發( Table dispatch )

這種調用方式利用一個表,該表是一組函數指針,稱爲witness table,以查找特性方法的實現。

witness table如何工做?

  • 每一個子類都有它本身的表結構
  • 對於類中每一個重寫的方法,都有不一樣的函數指針
  • 當子類添加新方法時,這些方法指針會添加在表數組的末尾
  • 最後,編譯器在運行時使用此表來查找調用函數的實現

因爲編譯器必須從表中讀取方法實現的內存地址,而後跳轉到該地址,所以它須要兩條附加指令,所以它比靜態分派慢,但仍比消息分派快。

注意:我不太肯定,可是這種特殊的派發技術能夠是虛擬派發,由於它利用了虛擬表,可是我找不到具體的參考。

  1. 消息派發( Message dispatch )

這種動態派發方式是最動態的。事實上,它表現優異(省去了優化部分),目前,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)。這很慢,爲了提供效率,咱們使用緩存來儘量的讓經常使用的方法被快速找到。

舉例:

值類型 (Value type)
struct Person {
   func isIrritating() -> Bool { }  // Static
}
extension Person {
   func canBeEasilyPissedOff() -> Bool { } // Static
}
複製代碼

因爲structenum都是值類型, 不支持繼承,編譯器將他們置爲靜態派發下,由於他們永遠不可能被子類化。

協議 (Protocol)
Protocol Animal {
   func isCute() -> Bool { } // Table
}
extension Animal {
   func canGetAngry() -> Bool { } // Static
}
複製代碼

這裏的重點是在extenison(擴展)裏面定義的函數,使用靜態派發(static dispatch)

類 (Class)
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
}
複製代碼
  • 普通方法聲明遵循協議的規則
  • 當咱們將方法公開給Objecitve-C runtime時用@objc,使用靜態派發(static dispatch)
  • 當一個類被標記爲final時,該類不能被子類化,由於使用靜態派發(static dispatch)

好吧,如今這只是我在講,您相信我所說的一切,對嗎?

如今如何證實這些方法其實是使用我上面解釋的派發技術?

爲此,咱們必須看一下Swift中間語言(SIL)。經過我在網上能夠進行的研究,我發現有一種方法:

  1. 若是函數使用Table派發,則它會出如今vtable(或witness_table)中
sil_vtable Animal { 
#Animal.isCute!1:(Animal)->()->():main.Animal.isCute()->()// Animal.isCute()
…… 
}
複製代碼
  1. 若是函數使用Message Dispatch,則關鍵字volatile應該存在於調用中。另外,您將找到兩個標記foreignobjc_method,指示使用Objective-C運行時調用了該函數。
%14 = class_method [volatile]%13:$ Dog,#Dog.goWild!1.foreign:(Dog)->()->(),$ @ convention(objc_method)(Dog)->() 
複製代碼
  1. 若是沒有以上兩種狀況的證據,答案是靜態派發

好吧,就是我這邊!我計劃這是一個由兩篇文章組成的系列文章,而下一篇文章(如今能夠在 此處得到)將涉及經過測試用例進行靜態和動態派發之間的性能比較。
相關文章
相關標籤/搜索