Swift函數派發機制

派發機制

在探究Swift函數派發機制以前,咱們應該先了解一下函數派發的基本知識。函數派發就是程序判斷使用哪一種途徑去調用一個函數的機制,也就是CPU在內存中找到該函數地址並調用的過程。每次函數被調用時都會被觸發, 但你又不會太留意的一個東西. 瞭解函數派發機制對於寫出高性能的代碼來講頗有必要, 並且也可以解釋不少 Swift 裏"奇怪"的行爲。swift

編譯型語言有三種基礎的函數派發方式:數組

  • 直接派發(Direct Dispatch,有人也稱爲靜態派發,下文中咱們統一叫直接派發)
  • 函數表派發(Table Dispatch)
  • 消息機制派發(Message Dispatch)。

大多數語言都會支持一到兩種, Java 默認使用函數表派發, 但你能夠經過 final 修飾符修改爲直接派發。 C++ 默認使用直接派發, 但能夠經過加上 virtual 修飾符來改爲函數表派發。 而 Objective-C 則老是使用消息機制派發, 但容許開發者使用 C 直接派發來獲取性能的提升。緩存

直接派發 (Direct Dispatch)

直接派發是三種派發方式中最快的。CPU直接按照函數地址調用,使用最少的指令集,辦最快的事情。當編譯器對程序進行優化的時候,也經常將函數內聯,使之成爲直接派發方式,優化執行速度。咱們熟知的C++默認使用直接派發方式,在Swift中給函數加上final關鍵字,該函數也會變成直接派發的方式。固然,有利就有弊,直接派發最大的弊病就是沒有動態性,不支持繼承。安全

函數表派發 (Table Dispatch)

這種方式是編譯型語言最多見的派發方式,他既保證了動態性也兼顧了執行效率。函數所在的類會維護一個」函數表」,也就是咱們熟知的虛函數表,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 安全地插入函數。佈局

消息機制派發 (Message Dispatch)

消息機制是調用函數最動態的方式. 也是 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 的派發機制如何派發函數

瞭解了函數派發的基本知識,咱們來看看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

添加了final關鍵字的函數沒法被重寫,使用直接派發,不會在vtable中出現。而且對Objc runtime不可見

dynamic

值類型和引用類型的函數都可添加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可見。

@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 or static

@inline關鍵字顧名思義是想告訴編譯器將此函數直接派發,但將其轉換成SIL代碼後,依舊是vtable派發。Static關鍵字會將函數變爲直接派發。

編譯器優化

Swift會盡量的去優化函數派發方式。咱們上文提到,當一個類聲明瞭一個私有函數時,該函數極可能會被優化爲直接派發。

派發總結

最後咱們用一張圖總結下Swift中的派發方式:

從上表可見,咱們在類型的主體中聲明的函數大都是函數表派發,這也是Swift中最爲常見的派發方式;而擴展大都是直接派發;只有再添加了特定關鍵字後,如@objc, final, dynamic後,函數派發方式纔會有所改變。除此以外,編譯器可能將某些方法優化爲直接派發。例如私有函數。

相關文章
相關標籤/搜索