Function Invocation:vtable dispatch and message dispatch, what’s different?(晶晶體)

春麗 S.T.E.M.
14 min readAug 20, 2023

--

目錄
1
. vtable dispatch
1-1 protocol
1-2 protocol extension(witness table)
2. SIL
3. selector
4. message dispatch(swizzling)

⦿ vtable dispatch
⦿ protocol
⦿ protocol extension(witness table)
⦿ SIL
⦿ selector
⦿ message dispatch(swizzling)

vtable dispatch

vtable dispatch,Virtual Table Dispatch,虛擬表分派,在 OOP(物件導向) Programming Language, like C++、Swift,當該程式語言中定義了一個或多個虛擬方法,會建立一個虛擬表(Virtual Table),這個表會將每個方法映射到該方法的記憶體位址

在 Invoke 的時候,會去查找虛擬表執行該方法,這種方式在 compile 時已定下來,比其他機制(如 message dispatch)更加快速。

protocol

在 Swift 中,是這樣的達到動態分派的:

protocol Showable {
func show()
}

class Base: Showable {
func show() {
print("This is base protocol.")
}
}

class Derived: Base {

}

override func viewDidLoad() {
super.viewDidLoad()
let base: Showable = Base()
base.show() // 輸出:This is base protocol.

let derived: Showable = Derived()
derived.show() // 輸出:This is base protocol.
}

當你建立了一個 Protocol,裡面寫了一個 func,你不用先實作這個 func ,當 class 遵循這個 Protocol,必須實作這個 func。

如果另一個 class inherit 這個 class,這個 func 也會被繼承,分別為兩個 class 產生實例,並各自 invoke 這個 func,都會 invoke Base 裡的 func。

倘若在 Derived 中,我們 override 繼承的 func,如下:

class Base: Showable {
func show() {
print("This is base protocol.")
}
}

class Derived: Base {
override func show() {
print("This is derived class.")
}
}

override func viewDidLoad() {
super.viewDidLoad()
let base: Showable = Base()
base.show() // 輸出:This is base protocol.

let derived: Showable = Derived()
derived.show() // 輸出:This is derived class.
}

就會 invoke 各自實作的 func,如果你不想放棄本來的 func 所做的事,我們會這樣寫:

class Derived: Base {
override func show() {
super.show()
print("This is derived class.")
}
}

override func viewDidLoad() {
super.viewDidLoad()
let derived: Showable = Derived()
derived.show() // 輸出:This is base protocol.
// 輸出:This is derived class.
}

這是不是跟 viewDidLoad 裡的 super.viewDidLoad 很像呢?是的。

protocol extension(witness table)

如果我們對某個協議加上 extension,如下:

protocol Showable {
func show()
}

extension Showable {
func show() {
print("This is base protocol.")
}
}

class Base: Showable {

}

class Derived: Base {
func show() {
print("This is derived class.")
}

}

override func viewDidLoad() {
super.viewDidLoad()
let base: Showable = Base()
base.show() // 輸出:This is base protocol.

let derived: Showable = Derived()
derived.show() // 輸出:This is base protocol.
}

就會 invoke 協議新增的預設方法,但這不是很奇怪嗎?base 沒問題,它並沒有另外實作,而 derived 並沒有 invoke 它實作的方法。

我們必須回頭去想 Swift 是一個型別安全的語言,希望開發者設置屬性時明確宣告它的型別,減少一些隱性轉型、Buffer Overflow(緩衝區溢出),更重要的是在 compile 時就能檢查是否有誤。

    let derived: Showable = Derived()
derived.show() // 輸出:This is base protocol.

所以這種宣告方式是在說,我要專注在遵循協議的型別上,僅協議的部份,而不去管這個型別裡有什麼其他東西。

如果你又想 invoke 該型別的其他方法,我們會這樣操作:

       let derived: Showable = Derived()
derived.show() // 輸出:This is base protocol.
if let someDerived = derived as? Derived {
someDerived.show() // 輸出:This is derived class.
}

如此一來變得相當麻煩,但也相對安全。這兩行宣告方式,一個是說我專注在該協議上,一個是說我專注在該型別上,看到如下:

class Derived: Base {
func show() {
print("This is derived class.")
}
func fun() {
print("123")
}

}

let derived: Showable = Derived()
derived.show() // 輸出:This is base protocol.
if let someDerived = derived as? Derived {
someDerived.show() // 輸出:This is derived class.
someDerived.fun() // 輸出:123.
}

是不是很有趣呢?

然而,其實 Protocol 的用法在 Swift 中,是 Witness Table Dispatch,跟 vtable dispatch 還是有些不同的,要探討 vtable dispatch 我們應專注在 class 上面。

回目錄

繼續閱讀|回目錄

SIL

SIL,Swift Intermediate Language,是介於 Swift CodeLLVM IR 的中介,SIL 幫助 Swift 在 compile 前便能夠檢查出是否有誤,而透過 vtable、witness table 來知道要 invoke 哪個方法。

我們可以用 SIL 來看 vtable 如何運作,在 CLI 輸入如下:

chunlicheng@MacBook-Air-M1 ~ % swiftc -emit-sil /Users/chunlicheng/Desktop/SILTesting.swift

意思是將你的 Swift 轉為 SIL。如果有出錯會告訴你,沒有的話,會得到下面的結果,不過,我們只看 vtable 的部份:

sil_stage canonical

import Builtin
import Swift
import SwiftShims

protocol Showable {
func show()
}

extension Showable {
func show()
}

class Base : Showable {
@objc deinit
init()
}

@_inheritsConvenienceInitializers class Derived : Base {
func show()
func fun()
override init()
@objc deinit
}


sil_vtable Base {
#Base.init!allocator: (Base.Type) -> () -> Base : @$s10SILTesting4BaseCACycfC // Base.__allocating_init()
#Base.deinit!deallocator: @$s10SILTesting4BaseCfD // Base.__deallocating_deinit
}

sil_vtable Derived {
#Base.init!allocator: (Base.Type) -> () -> Base : @$s10SILTesting7DerivedCACycfC [override] // Derived.__allocating_init()
#Derived.show: (Derived) -> () -> () : @$s10SILTesting7DerivedC4showyyF // Derived.show()
#Derived.fun: (Derived) -> () -> () : @$s10SILTesting7DerivedC3funyyF // Derived.fun()
#Derived.deinit!deallocator: @$s10SILTesting7DerivedCfD // Derived.__deallocating_deinit
}

sil_witness_table hidden Base: Showable module SILTesting {
method #Showable.show: <Self where Self : Showable> (Self) -> () -> () : @$s10SILTesting4BaseCAA8ShowableA2aDP4showyyFTW // protocol witness for Showable.show() in conformance Base
}

在 vtable 中,說明初始化以及銷毀 Base 時,會 invoke 的方法;接著可以看到 Derived 繼承了 Base 會重寫初始化方法;而不論在 Derived 或在 Protocol 中,也說明了 invoke 方法時會去查表,這就說明了 vtable 與 witness table 的作用。

vtable dispatch 與 witness table dispatch,甚至 message dispatch 其實都是動態分派,差別僅在於 vtable 與 witness table 是在 compile 時就能確立,而 message dispatch 則是在 runtime 時才能確立。

那麼,在 Swift 中,什麼時候會用 message dispatch 呢?我們需要回頭講 Objective-C。

回目錄

繼續閱讀|回目錄

selector

在 Xcode 開發撰寫純程式碼的時候,我們很常用到 UIButton,在 addTarget 的地方,是透過 selector 去橋接 Objective-C 的程式碼,最大的不同點在於 Swift 用 vtable dispatch,而 Objective-C 用 message dispatch,後者必須在 runtime 才能夠確定方法的調用。

先看到如下:

let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)
button.center = view.center
button.backgroundColor = .orange

@objc func buttonTapped() {
print("Button was tapped!")
}

當我們在用 Objective-C 調用一個方法時,實際上是傳送一個 message 到物件上,接著動態查找並執行相應的方法,所以 Objective-C 允許做出方法的動態替換,比 Swift 靈活些,只是比較慢,而 Swift 相對安全並可提前知道哪有錯誤。

回目錄

繼續閱讀|回目錄

message dispatch (swizzling)

如果要使用 Swift 動態替換方法可如下操作:

class MyClass: NSObject {
@objc dynamic func originalMethod() {
print("This is the original method")
}
}


let method = class_getInstanceMethod(MyClass.self, #selector(MyClass.originalMethod))
let originalImplementation = method_getImplementation(method!)

let myObject = MyClass()
myObject.originalMethod() // Prints: "This is the original method"

let newImplementationBlock: @convention(block) (AnyObject) -> Void = { _ in
print("This is the replacement method")
}
let newImplementation = imp_implementationWithBlock(newImplementationBlock)
method_setImplementation(method!, newImplementation)

myObject.originalMethod() // Prints: "This is the replacement method"

在 class 裡,我們需要一個 dynamic 的方法。

class_getInstanceMethod 是到 MyClass 裡去找 originalMethod 這個方法的指標,比方說 0x0000000102f084e1,若沒有則回傳 nil。

接著,method_getImplementation 是從前一個指標,找到真正實作的指標(IMP),與前一個指標是不一樣的。

再來,你實作一個新的閉包,用 @convention(block) 與 Objective-C 橋接,後面就可以寫成 Swift 閉包的形式:

(AnyObject) -> Void = { _ in
print("This is the replacement method")
}

再將新的閉包的 IMP 帶入 method_setImplementation,動態調用函式就完成了!這種技巧叫做 swizzlizg,方法替換。輸出結果如下:

This is the original method
This is the replacement method

OKay!完成了!

但 message dispatch 的缺點在 runtime 才能夠除錯,如果你沒寫對的話,比方說,我們新的閉包改成如下:

let newImplementationBlock: @convention(block) (String) -> Void = { (stringValue) in
print("This is the replacement method\(stringValue)")
}

實際上並沒有 String,但這在 Compile 中並不會發現,等到 build 時:

是不是很棒呢?我們對 vswitch dispatch 跟 message dispatch 有些概念了!

這次就分享到這,感謝您的閱讀。

回目錄

繼續閱讀|回目錄

附上 GitHub:

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet