Swift 的派發機制與思考題

簡介

dispatch部分是根據其餘人的文章整理的。git

思考題部分是在項目中遇到,但願你們在看完派發後能有所思考。github

dispatch介紹

咱們都知道一個方法會在運行時被調用,一個方法被喚起,是由於編譯器有一個計算機制,用來選擇正確的方法,而後經過傳遞參數來喚起它.編程

這個機制一般被成爲派發(dispatch). 分派就是處理方法調用的過程.swift

分派在處理方法調用的時候,可能會存在多個合理的可被調用的方法列表,此時就須要去選擇最正確的方法.選擇正確方法的整個過程,就是派發(dispatch). 每種編程語言都須要分派機制來選擇正確的喚起方法.後端

方法從書寫完成到調用完成,歸納上會經歷編譯期和運行期兩個階段,而前面說的x選擇哪一個方法被執行,也是在這兩個時期進行的.數組

選擇正確方法的階段,能夠分爲編譯期和運行期,而分派機制經過這兩個不一樣的時期分爲兩種: 靜態分派(static dispatch)和 動態派發(dynamic dispatch).安全

static dispatch能夠確保某個方法只有一種實現.static dispatch明顯的快於dynamic dispatch,由於dynamic dispatch自己就意味着較高的性能開銷.bash

dymanic dispatch,是基於運行期的給定信息來肯定調用方法的,可能經過虛函數表實現,也可能借助其餘的運行期的信息.app

static dispatch

static dispatch是在編譯期就徹底肯定調用方法的分派方式.編程語言

用於在多態狀況下,在編譯期就實現對於肯定的類型,在函數調用表中推斷和追溯正確的方法,包括列舉泛型的特定版本,在提供的所有函數定義中選擇的特定實現.

在編譯器肯定使用static dispatch後,會在生成的可執行文件內,直接指定包含了方法實現內存地址的指針,編譯器直接找到相關指令的位置。當函數調用時,系統直接跳轉到函數的內存地址執行操做。 這樣的好處是,調用指令少,執行快,同時容許編譯器可以執行例如內聯等優化,缺點是因爲缺乏動態性而不支持繼承。 事實上,編譯期在編譯階段爲了可以獲取最大的性能提高,都儘可能將函數靜態化。

dynamic dispatch

dynamic dispatch是 用於在運行期選擇調用方法的實現的流程. dynamic dispatch被普遍應用,而且被認爲是面嚮對象語言的基本特性. OOP是經過名稱來查找對象和方法的.可是多態就是一種特殊狀況了,由於可能會出現多個同名方法,可是內部實現各不相同.若是把OOP理解爲向對象發送消息的話.在多態模式下,就是程序向不知道類型的對象發送了消息,而後在運行期再將消息分派給正確的對象.以後對象再肯定執行什麼操做. 與static dispatch在編譯期肯定最終執行不一樣,dynamic dispatch的目的是爲了支持在編譯期沒法肯定最終最合適的實現的操做.這種狀況通常是由於在運行期才能經過一個或多個參數肯定對象的類型.例如 B繼承自A, 聲明var obj : A = B(),編譯期會認爲是A類型,可是真正的類型B,只能在運行期肯定.

一種語言可能有多種dynamic dispatch的實現機制.語言的特性不一樣,動態分派的實現也各有差別.

消息機制派發(Message Dispatch)

消息機制派發 (Message Dispatch): Objc的函數派發都是基於消息派發的。這種機制極具動態性,既能夠經過swizzling修改函數的實現,也能夠經過isa-swizzling修改對象。

調用流程
經過消息派發執行子類中的函數的步驟: 1.到本身的方法列表中去找,若是找到了,執行對應邏輯,若是沒找到執行2。 2.去它的父類中去找,發現找到了,就執行相應的邏輯。
派發流程

函數表派發 (Table Dispatch)

函數表派發是編譯型語言實現動態行爲最多見的實現方式。 函數表使用了一個數組來存儲類聲明的每個函數的指針。大部分語言把這個稱爲 「virtual table」(虛函數表),Swift 裏稱爲 「witness table」。 每個類都會維護一個函數表,裏面記錄着類全部的函數,若是父類函數被override,表裏面只會保存被 override 以後的函數。 一個子類新添加的函數,都會被插入到這個數組的最後。運行時會根據這一個表去決定實際要被調用的函數。

一個函數被調用時會先去讀取對象的函數表 再根據類的地址加上該的函數的偏移量獲得函數地址 最後跳到那個地址上去。 從編譯後的字節碼這方面來看就是兩次讀取一次跳轉,比直接派發仍是慢了些。

這種基於數組的實現, 缺陷在於函數表沒法拓展. 子類會在虛數函數表的最後插入新的函數, 沒有位置可讓 extension 安全地插入函數.

派發流程

編譯器的優化

編譯器能夠經過whole module optimization檢查繼承關係,對某些沒有標記final的類經過計算,若是能在編譯期肯定執行的方法,則使用Static dispatch。 好比一個函數沒有 override,Swift 就可能會使用直接派發的方式,因此若是屬性綁定了 KVO 它的 getter 和 setter 方法可能會被優化成直接派發而致使 KVO 的失效,因此記得加上 dynamic 的修飾來保證有效。

sil指令

編譯器內部運行過程分爲:語法分析,類型檢查,SIL優化,LLVM後端處理。 這是生成sil文件的指令,能夠經過查閱SIL官方文檔瞭解文件中各類指令的含義。

swiftc -emit-sil main.swift | xcrun swift-demangle > main.sil

swiftc -emit-silgen -o main.swift | xcrun swift-demangle > main.silgen

源代碼:

class A {
    let a = 1
    var b = 1
    private var c = 1
    
    func testA() {
        
    }
}
let a = A()
a.testA()
複製代碼

生成的sil 文件:

sil_stage canonical

import Builtin
import Swift
import SwiftShims

class A {
  @_hasInitialValue @_hasStorage final let a: Int { get }
  @_hasInitialValue @_hasStorage var b: Int { get set }
  @_hasInitialValue @_hasStorage private var c: Int { get set }
  func testA()
  init()
  @objc deinit
}

@_hasInitialValue @_hasStorage let a: A { get }

// a
sil_global hidden [let] @main.a : main.A : $A

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.a : main.A                  // id: %2
  %3 = global_addr @main.a : main.A : $*A        // users: %8, %7
  %4 = metatype $@thick A.Type                    // user: %6
  // function_ref A.__allocating_init()
  %5 = function_ref @main.A.__allocating_init() -> main.A : $@convention(method) (@thick A.Type) -> @owned A // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick A.Type) -> @owned A // user: %7
  store %6 to %3 : $*A                            // id: %7
  %8 = load %3 : $*A                              // users: %9, %10
  %9 = class_method %8 : $A, #A.testA!1 : (A) -> () -> (), $@convention(method) (@guaranteed A) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (@guaranteed A) -> ()
  %11 = integer_literal $Builtin.Int32, 0         // user: %12
  %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
  return %12 : $Int32                             // id: %13
} // end sil function 'main'

......
sil_vtable A {
  #A.b!getter.1: (A) -> () -> Int : @main.A.b.getter : Swift.Int // A.b.getter
  #A.b!setter.1: (A) -> (Int) -> () : @main.A.b.setter : Swift.Int // A.b.setter
  #A.b!modify.1: (A) -> () -> () : @main.A.b.modify : Swift.Int // A.b.modify
  #A.c!getter.1: (A) -> () -> Int : @main.A.(c in _12232F587A4C5CD8B1EEDF696793A4FC).getter : Swift.Int // A.c.getter
  #A.c!setter.1: (A) -> (Int) -> () : @main.A.(c in _12232F587A4C5CD8B1EEDF696793A4FC).setter : Swift.Int // A.c.setter
  #A.c!modify.1: (A) -> () -> () : @main.A.(c in _12232F587A4C5CD8B1EEDF696793A4FC).modify : Swift.Int // A.c.modify
  #A.testA!1: (A) -> () -> () : @main.A.testA() -> () // A.testA()
  #A.init!allocator.1: (A.Type) -> () -> A : @main.A.__allocating_init() -> main.A // A.__allocating_init()
  #A.deinit!deallocator.1: @main.A.__deallocating_deinit // A.__deallocating_deinit
}
複製代碼

class_method是經過函數表派發的函數。

sil_vtable A是A中函數表的內容。

更多詳情能夠查閱文檔。

思考題

第一題

只聲明屬性的狀況下,會在函數表中生成方法麼?有幾種方法? 若是var、public、 final等屬性修飾後會發生什麼事?

第二題

static func exchangeFunc() {
        let originSelector = #selector(ASViewController.init(nibName:bundle:))
        let exchangeSelector = #selector(ASViewController.baiduInit(nibName:bundle:))
        guard let m1 = class_getInstanceMethod(self, originSelector) else {
            return
        }
        guard let m2 = class_getInstanceMethod(self, exchangeSelector) else {
            return
        }
        method_exchangeImplementations(m1, m2)
    }

複製代碼

這個是一個簡單的方法交換的示例。 問題:init(nibName:bundle:)是一個初始化方法,爲何經過class_getInstanceMethod去獲取而不是class_getClassMethod? 初始化方法是否有實例方法與類方法的區別?

第三題

class A {
    static func exchangeFunc() {
        let originSelector = #selector(A.printA)
        let exchangeSelector = #selector(A.printB)
        guard let m1 = class_getInstanceMethod(self, originSelector) else {
            return
        }
        guard let m2 = class_getInstanceMethod(self, exchangeSelector) else {
            return
        }
        method_exchangeImplementations(m1, m2)
    }
    
    @objc
    func printA() {
        print("A")
    }
    
    @objc
    func printB() {
        print("B")
    }
}

A.exchangeFunc()
let a = A()
a.printA() // A
複製代碼

問題:

爲何方法替換的操做在執行後沒有生效?

若是但願生效應該怎麼作?

第四題

class C1: UIView {
    
    func test1()
}

class C2: C1 {
    
    override func layoutSubview() {
// ...
    }
    
    override func test1() {
        
    }
}
複製代碼

上面的方法派發機制是什麼?

第五題

// Defined protocol。
protocol A {
    func a() -> Int
}
extension A {
    func a() -> Int {
        return 0
    }
}

// A class doesn't have implement of the function。
class B: A {}

class C: B {
    func a() -> Int {
        return 1
    }
}

B().a() 
C().a() 
(C() as A).a() 
複製代碼

分別寫出以上方法運行後的輸出,以及緣由。

第六題

可否在override在extension中定義的方法?

可否在extension中override方法?

第七題

小張在工做中實現了相似於第五題的寫法。

基類實現一個tableview並繼承協議,且實現了其中必須實現的協議。

在子類中,override以及實現的協議或者添加新的tableview協議中的方法。

結果程序正常運行,並無出現bug。

這是爲何?

忽然有一天出現了bug,小張修改了Xcode某個屬性,程序就能正常運行了。

修改了什麼屬性?

相關文章
相關標籤/搜索