在探究Swift函數派發機制以前,咱們應該先了解一下函數派發的基本知識。函數派發就是程序判斷使用哪一種途徑去調用一個函數的機制,也就是CPU在內存中找到該函數地址並調用的過程。每次函數被調用時都會被觸發, 但你又不會太留意的一個東西. 瞭解函數派發機制對於寫出高性能的代碼來講頗有必要, 並且也可以解釋不少 Swift 裏"奇怪"的行爲。swift
編譯型語言有三種基礎的函數派發方式:數組
大多數語言都會支持一到兩種, Java 默認使用函數表派發, 但你能夠經過 final 修飾符修改爲直接派發。 C++ 默認使用直接派發, 但能夠經過加上 virtual 修飾符來改爲函數表派發。 而 Objective-C 則老是使用消息機制派發, 但容許開發者使用 C 直接派發來獲取性能的提升。緩存
直接派發是三種派發方式中最快的。CPU直接按照函數地址調用,使用最少的指令集,辦最快的事情。當編譯器對程序進行優化的時候,也經常將函數內聯,使之成爲直接派發方式,優化執行速度。咱們熟知的C++默認使用直接派發方式,在Swift中給函數加上final關鍵字,該函數也會變成直接派發的方式。固然,有利就有弊,直接派發最大的弊病就是沒有動態性,不支持繼承。安全
這種方式是編譯型語言最多見的派發方式,他既保證了動態性也兼顧了執行效率。函數所在的類會維護一個」函數表」,也就是咱們熟知的虛函數表,Swift 裏稱爲 "witness table"。該函數表存取了每一個函數實現的指針。每一個類的vtable在編譯時就會被構建,因此與直接派發相比只多出了兩個讀取的工做: 讀取該類的vtable和該函數的指針。理論上說,函數表派發也是一種高效的方式。不過和直接派發相比,編譯器對某些含有反作用的函數卻沒法優化,也是致使函數表派發變慢的緣由之一。並且Swift類擴展裏面的方法沒法動態加入該類的函數表中,只能使用靜態派發的方式,這也是函數表派發的缺陷之一。bash
咱們來看以下代碼:app
class ParentClass {
func method1() {}
func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
func method3() {}
}
複製代碼
當前情景下,編譯器會建立兩個函數表: 一個屬於 ParentClass 類,另外一個屬於 ChildClass 類,內存佈局以下:ide
let obj = ChildClass()
obj.method2()
複製代碼
當調用函數 method2() 時,過程以下:函數
讀取該對象(0XB00)的vtable.
讀取method2函數指針0x222.
跳轉到地址0X222,讀取函數實現.
複製代碼
查表是一種簡單, 易實現, 並且性能可預知的方式. 然而, 這種派發方式比起直接派發仍是慢一點. 從字節碼角度來看, 多了兩次讀和一次跳轉, 由此帶來了性能的損耗. 另外一個慢的緣由是咱們開頭也說了,編譯器對某些含有反作用的函數卻沒法優化,也是致使函數表派發變慢的緣由之一。 這種基於數組的實現, 缺陷在於函數表沒法拓展. 子類會在虛數函數表的最後插入新的函數, 沒有位置可讓 extension 安全地插入函數。佈局
消息機制是調用函數最動態的方式. 也是 Cocoa 的基石, 這樣的機制催生了 KVO, UIAppearence 和 CoreData 等功能. 這種運做方式的關鍵在於開發者能夠在運行時改變函數的行爲. 不止能夠經過 swizzling 來改變, 甚至能夠用 isa-swizzling 修改對象的繼承關係, 能夠在面向對象的基礎上實現自定義派發。性能
因爲Swfit使用的依舊是Objc的運行時系統,因此這裏的消息派發其實也就是Objc的Message Passing(消息傳遞)。
id returnValue = [someObject messageName:parameter];
複製代碼
someObject就是接收者,messageName就是選擇器,選擇器和參數一塊兒被稱爲 「消息「。 當編譯時,編譯器會將該消息轉換成一條標準的C語言調用:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
複製代碼
objc_msgSend函數回一句接收者和選擇器的類型來調用適當的方法,它會去接收者所屬類中搜索其方法列表,若是能找到,則跳轉到對應實現;若找不到,則沿着繼承體系繼續向上查找,若能找到,則跳轉;若是最終仍是找不到,那就執行邊界狀況的操做,例如 Message forwarding(消息轉發)。 看看下面的代碼:
Swift 會用樹來構建這種繼承關係:
class ParentClass {
dynamic func method1() {}
dynamic func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
dynamic func method3() {}
}
複製代碼
這種派發方式的流程步驟彷佛不少,所幸的是objc_msgSend會將匹配的結果緩存到fast map(快速映射表)中,並且每一個類都有這樣一塊緩存;如果以後發送相同的消息,執行速率會很快,會把性能提升到和函數表派發同樣快。
瞭解了函數派發的基本知識,咱們來看看Swift如何處理函數派發以及如何證實該種派發。咱們先來看一張總結表:
從上表中咱們能夠直觀的總結出:函數的派發方式和如下兩點相關聯:
對象類型; 值類型老是使用直接派發(靜態派發,由於他們沒有繼承體系)
函數聲明的位置; 直接在定義中聲明和在擴展中(extension)聲明
複製代碼
除此以外,顯式的指定派發方式也會改變函數其原有的派發方式,例如添加final或者@objc關鍵字等等;以及編譯器對特定函數的優化,例如將從未被重寫的私有函數優化成靜態派發。
下面咱們就這四個方面來分析和探討Swift的派發方式,以及證實其派發方式。
如上文所述,值類型,也就是struct的對象老是使用靜態派發; class對象使用函數表派發(非extension)。請看以下代碼示例:
class MyClass {
func testOfClass() {}
}
struct MyStruct{
func testOfStruct() {}
}
複製代碼
如今咱們使用以下命令將swift代碼轉換爲SIL(中間碼)以便查看其函數派發方式:
swiftc -emit-silgen -O main.swift
複製代碼
輸出結果以下:
...
class MyClass {
func testOfClass()
@objc deinit ///新增
init() //新增
}
struct MyStruct {
func testOfStruct()
init() //新增
}
/// sil_vtable
sil_vtable MyClass {
#MyClass.testOfClass!1: (MyClass) -> () -> () : @$s4main7MyClassC06testOfC0yyF // MyClass.testOfClass()
#MyClass.init!allocator.1: (MyClass.Type) -> () -> MyClass : @$s4main7MyClassCACycfC // MyClass.__allocating_init()
#MyClass.deinit!deallocator.1: @$s4main7MyClassCfD // MyClass.__deallocating_deinit
}
複製代碼
首先swift會爲class添加init和@objc deinit方法,爲struct添加init方法。在文件的結尾處就會顯示如上代碼,它展現了哪些函數是函數表派發的,以及它們的標識符。因爲struct類型僅使用靜態派發,因此不會顯示sil_vtable字樣。
函數聲明位置的不一樣也會致使派發方式的不一樣。在Swift中,咱們經常在extension裏面添加擴展方法。根據咱們以前總結的表格,一般extension中聲明的函數都默認使用靜態派發。
protocol MyProtocol {
func testOfProtocol()
}
extension MyProtocol {
func testOfProtocolInExtension() {}
}
class MyClass: MyProtocol {
func testOfClass() {}
func testOfProtocol() {}
}
extension MyClass {
func testOfClassInExtension() {}
}
複製代碼
咱們分別在protocol和class中聲明一個函數,再在其extension中聲明一個函數; 最後讓類實現協議的一個方法,轉換成SIL代碼後以下:
protocol MyProtocol {
func testOfProtocol()
}
extension MyProtocol {
func testOfProtocolInExtension()
}
class MyClass : MyProtocol {
func testOfClass()
func testOfProtocol()
@objc deinit
init()
}
extension MyClass {
func testOfClassInExtension()
}
...
///sil_vtable
sil_vtable MyClass {
#MyClass.testOfClass!1: (MyClass) -> () -> () : @$s4main7MyClassC06testOfC0yyF // MyClass.testOfClass()
#MyClass.testOfProtocol!1: (MyClass) -> () -> () : @$s4main7MyClassC14testOfProtocolyyF // MyClass.testOfProtocol()
#MyClass.init!allocator.1: (MyClass.Type) -> () -> MyClass : @$s4main7MyClassCACycfC // MyClass.__allocating_init()
#MyClass.deinit!deallocator.1: @$s4main7MyClassCfD // MyClass.__deallocating_deinit
}
///sil_witness_table
sil_witness_table hidden MyClass: MyProtocol module main {
method #MyProtocol.testOfProtocol!1: <Self where Self : MyProtocol> (Self) -> () -> () : @$s4main7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW // protocol witness for MyProtocol.testOfProtocol() in conformance MyClass
}
複製代碼
咱們能夠很直觀的看到,聲明在協議或者類主體中的函數是使用函數表派發的; 而聲明在擴展中的函數則是靜態派發。
值得注意的是: 當咱們在protocol中聲明一個函數,而且在protocol的extension中實現了它,並且沒有其餘類型重寫該函數,那麼在這種狀況下,該函數就是直接派發,算是通用函數。
給函數添加關鍵字的修飾也能改變其派發方式。
添加了final關鍵字的函數沒法被重寫,使用直接派發,不會在vtable中出現。而且對Objc runtime不可見
值類型和引用類型的函數都可添加dynamic關鍵字。在Swift5中,給函數添加dynamic的做用是爲了賦予非objc類和值類型(struct和enum)動態性。咱們來看以下代碼:
struct Test {
dynamic func test() {}
}
複製代碼
咱們賦予了test函數動態性。將其轉換成SIL中間碼後以下:
// Test.test()
sil hidden [dynamically_replacable] [ossa] @$s4main4TestV4testyyF : $@convention(method) (Test) -> () {
// %0 // user: %1
bb0(%0 : $Test):
debug_value %0 : $Test, let, name "self", argno 1 // id: %1
%2 = tuple () // user: %3
return %2 : $() // id: %3
} // end sil function '$s4main4TestV4testyyF'
複製代碼
咱們在第二行能夠看到test函數多了一個「屬性」: dynamically_replacable, 也就是說添加dynamic關鍵字就是賦予函數動態替換的能力。那什麼是動態替換呢? 簡而言之就是提供一種途徑,比方說,能夠將Module A中定義的方法,在Module B中動態替換,以下所示:
struct ModuleAStruct {
dynamic func testModuleAStruct(){
print("struct-testModuleAStruct")
}
}
extension ModuleAStruct{
@_dynamicReplacement(for: testModuleAStruct())
func testModuleAStructReplacement() {
print("extension-testModuleAStructReplacement")
}
}
let foo = ModuleAStruct()
foo.testModuleAStruct()
///經過調用測試打印出來的是
extension-testModuleAStructReplacement
複製代碼
注意: 添加dynamic關鍵字並不表明對Objc可見。
該關鍵字能夠將Swift函數暴露給Objc運行時,但並不會改變其派發方式,依舊是函數表派發。舉例以下:
class Test {
@objc func test() {}
}
複製代碼
SIL代碼以下:
...
// @objc Test.test()
sil hidden [thunk] [ossa] @$s4main4TestC4testyyFTo : $@convention(objc_method) (Test) -> () {
// %0 // user: %1
bb0(%0 : @unowned $Test):
%1 = copy_value %0 : $Test // users: %6, %2
%2 = begin_borrow %1 : $Test // users: %5, %4
// function_ref Test.test()
%3 = function_ref @$s4main4TestC4testyyF : $@convention(method) (@guaranteed Test) -> () // user: %4
%4 = apply %3(%2) : $@convention(method) (@guaranteed Test) -> () // user: %7
end_borrow %2 : $Test // id: %5
destroy_value %1 : $Test // id: %6
return %4 : $() // id: %7
} // end sil function '$s4main4TestC4testyyFTo'
...
sil_vtable Test {
#Test.test!1: (Test) -> () -> () : @$s4main4TestC4testyyF // Test.test()
#Test.init!allocator.1: (Test.Type) -> () -> Test : @$s4main4TestCACycfC // Test.__allocating_init()
#Test.deinit!deallocator.1: @$s4main4TestCfD // Test.__deallocating_deinit
}
複製代碼
咱們能夠看到test方法依舊在「虛函數列表」中,證實其實函數表派發。若是但願test函數使用消息派發,則須要額外添加dynamic關鍵字。
@inline關鍵字顧名思義是想告訴編譯器將此函數直接派發,但將其轉換成SIL代碼後,依舊是vtable派發。Static關鍵字會將函數變爲直接派發。
Swift會盡量的去優化函數派發方式。咱們上文提到,當一個類聲明瞭一個私有函數時,該函數極可能會被優化爲直接派發。
最後咱們用一張圖總結下Swift中的派發方式:
從上表可見,咱們在類型的主體中聲明的函數大都是函數表派發,這也是Swift中最爲常見的派發方式;而擴展大都是直接派發;只有再添加了特定關鍵字後,如@objc, final, dynamic後,函數派發方式纔會有所改變。除此以外,編譯器可能將某些方法優化爲直接派發。例如私有函數。