再探Swift函數(shù)的派發(fā)方式

Swift 的函數(shù)是怎么派發(fā)的呢? 我沒(méi)能找到一個(gè)很簡(jiǎn)明扼要的答案, 但這里有四個(gè)選擇具體派發(fā)方式的因素存在:

聲明的位置
引用類(lèi)型
特定的行為
顯式地優(yōu)化(Visibility Optimizations)
在解釋這些因素之前, 我有必要說(shuō)清楚, Swift 沒(méi)有在文檔里具體寫(xiě)明什么時(shí)候會(huì)使用函數(shù)表什么時(shí)候使用消息機(jī)制. 唯一的承諾是使用 dynamic 修飾的時(shí)候會(huì)通過(guò) Objective-C 的運(yùn)行時(shí)進(jìn)行消息機(jī)制派發(fā).
下面我寫(xiě)的所有東西, 都只是我在 Swift 5.0 里測(cè)試出來(lái)的結(jié)果, 并且很可能在之后的版本更新里進(jìn)行修改.

聲明的位置 (Location Matters)

在 Swift 里, 一個(gè)函數(shù)有兩個(gè)可以聲明的位置: 類(lèi)型聲明的作用域, 和 extension. 根據(jù)聲明類(lèi)型的不同, 也會(huì)有不同的派發(fā)方式。在Swift中,我們常常在extension里面添加擴(kuò)展方法砌溺。
首先看一個(gè)小問(wèn)題:

class MyClass {
    func mainMethod() {}
}
extension MyClass {
    func extensionMethod() {}
}

上面的例子里, mainMethod 會(huì)使用函數(shù)表派發(fā), 這一點(diǎn)是沒(méi)有任何異議的。
而 extensionMethod 則會(huì)使用直接派發(fā).
當(dāng)我第一次發(fā)現(xiàn)這件事情的時(shí)候覺(jué)得很意外, 直覺(jué)上這兩個(gè)函數(shù)的聲明方式并沒(méi)有那么大的差異.
為了搞清楚extension為什么是直接派發(fā)的問(wèn)題,我們?cè)倏匆粋€(gè)例子:

//首先聲明一個(gè)協(xié)議
protocol Drawing {
  func render()
}

//定義這個(gè)協(xié)議中的函數(shù)
extension Drawing {
  func circle() { print("protocol")}
  func render() { circle()}
}

//遵循這個(gè)協(xié)議
class SVG: Drawing {
  func circle(){ print("class") }
}

SVG().render()

// what's the output?

這里會(huì)輸出什么呢?
根據(jù)當(dāng)時(shí)的統(tǒng)計(jì),43%選擇了protocol, 57%選擇了class何什。但真理往往掌握在少數(shù)人手中,正確答案是protocol等龙。

objc給出的解釋是: circle函數(shù)聲明在protocol的extension里面处渣,所以不是動(dòng)態(tài)派發(fā), 并且類(lèi)沒(méi)有實(shí)現(xiàn)render函數(shù),所以輸出為protocol.
由此可以看出 : extension中聲明的函數(shù)是直接派發(fā)蛛砰,編譯的時(shí)候就已經(jīng)確定了調(diào)用地址罐栈,類(lèi)無(wú)法重寫(xiě)實(shí)現(xiàn),否則如果是函數(shù)表派發(fā)的話這里應(yīng)該輸出的是class泥畅,而不是protocol荠诬。

如果不相信實(shí)驗(yàn)的猜測(cè),那么我們可以直接編譯一下,看看到底是什么派發(fā)方式柑贞,使用如下命令將swift代碼轉(zhuǎn)換為SIL(中間碼)以便查看其函數(shù)派發(fā)方式:

? swiftc -emit-silgen -O main.swift
······
// MyClass.extensionMethod()
sil hidden [ossa] @$s4main7MyClassC15extensionMethodyyF : $@convention(method) (@guaranteed MyClass) -> () {
// %0                                             // user: %1
bb0(%0 : @guaranteed $MyClass):
  debug_value %0 : $MyClass, let, name "self", argno 1 // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function '$s4main7MyClassC15extensionMethodyyF'

sil_vtable MyClass {
  #MyClass.mainMethod!1: (MyClass) -> () -> () : @$s4main7MyClassC0A6MethodyyF  // MyClass.mainMethod()
  #MyClass.init!allocator.1: (MyClass.Type) -> () -> MyClass : @$s4main7MyClassCACycfC  // MyClass.__allocating_init()
  #MyClass.deinit!deallocator.1: @$s4main7MyClassCfD    // MyClass.__deallocating_deinit
}

我們可以很清楚的看到方椎,sil_vtable這張函數(shù)表里并沒(méi)有extensionMethod方法,因此可以斷定是直接派發(fā)钧嘶。

這里總結(jié)了一張表棠众,展示了默認(rèn)情況下Swift使用的派發(fā)方式:

類(lèi)型 初始聲明 extension
Value Type(值類(lèi)型) 直接派發(fā) 直接派發(fā)
Protocol(協(xié)議) 函數(shù)表派發(fā) 直接派發(fā)
Class(類(lèi)) 函數(shù)表派發(fā) 直接派發(fā)
NSObject Subclass(NSObject子類(lèi)) 函數(shù)表派發(fā) 消息機(jī)制派發(fā)

總結(jié)起來(lái)有這么幾點(diǎn):

值類(lèi)型總是會(huì)使用直接派發(fā), 簡(jiǎn)單易懂
協(xié)議和類(lèi)的 extension 都會(huì)使用直接派發(fā)
NSObject 的 extension會(huì)使用消息機(jī)制進(jìn)行派發(fā)
NSObject 聲明作用域里的函數(shù)都會(huì)使用函數(shù)表進(jìn)行派發(fā).
協(xié)議里聲明的,并且?guī)в心J(rèn)實(shí)現(xiàn)的函數(shù)會(huì)使用函數(shù)表進(jìn)行派發(fā)

引用類(lèi)型 (Reference Type Matters)

引用的類(lèi)型決定了派發(fā)的方式. 這很顯而易見(jiàn), 但也是決定性的差異. 一個(gè)比較常見(jiàn)的疑惑, 發(fā)生在一個(gè)協(xié)議拓展和類(lèi)型拓展同時(shí)實(shí)現(xiàn)了同一個(gè)函數(shù)的時(shí)候.

protocol MyProtocol {}

struct MyStruct: MyProtocol {}

extension MyStruct {
    func extensionMethod() {
        print("結(jié)構(gòu)體")
    }
}
extension MyProtocol {
    func extensionMethod() {
        print("協(xié)議")
    }
}
 
let myStruct = MyStruct()
let proto: MyProtocol = myStruct
 
myStruct.extensionMethod() // -> “結(jié)構(gòu)體”
proto.extensionMethod() // -> “協(xié)議”

剛接觸 Swift 的人可能會(huì)認(rèn)為 proto.extensionMethod() 調(diào)用的是結(jié)構(gòu)體里的實(shí)現(xiàn)。
但是有决,引用的類(lèi)型決定了派發(fā)的方式闸拿,協(xié)議拓展里的函數(shù)會(huì)使用直接派發(fā)方式調(diào)用。
如果把 extensionMethod 的聲明移動(dòng)到協(xié)議的聲明位置的話疮薇,則會(huì)使用函數(shù)表派發(fā)胸墙,最終就會(huì)調(diào)用結(jié)構(gòu)體里的實(shí)現(xiàn)。
并且按咒,如果兩種聲明方式都使用了直接派發(fā)的話迟隅,基于直接派發(fā)的運(yùn)作方式,我們不可能實(shí)現(xiàn)預(yù)想的 override 行為励七。

指定派發(fā)方式 (Specifying Dispatch Behavior)

Swift 有一些修飾符可以指定派發(fā)方式.

final or static

final和static 允許類(lèi)里面的函數(shù)使用直接派發(fā). 這個(gè)修飾符會(huì)讓函數(shù)失去動(dòng)態(tài)性.
任何函數(shù)都可以使用這個(gè)修飾符, 即使是 extension 里本來(lái)就是直接派發(fā)的函數(shù).
這也會(huì)讓 Objective-C 的運(yùn)行時(shí)獲取不到這個(gè)函數(shù), 不會(huì)生成相應(yīng)的 selector.
總之一句話:添加了final關(guān)鍵字的函數(shù)無(wú)法被重寫(xiě)(static可以被重寫(xiě))智袭,使用直接派發(fā),不會(huì)在函數(shù)表中出現(xiàn)掠抬,并且對(duì)Objc runtime不可見(jiàn)吼野。

dynamic

dynamic 可以讓類(lèi)里面的函數(shù)使用消息機(jī)制派發(fā). 使用 dynamic, 必須導(dǎo)入 Foundation 框架, 里面包括了 NSObject 和 Objective-C 的運(yùn)行時(shí).
dynamic 可以讓聲明在 extension 里面的函數(shù)能夠被 override.
dynamic 可以用在所有 NSObject 的子類(lèi)和 Swift 的原聲類(lèi).
在Swift5中,給函數(shù)添加dynamic的作用是為了賦予非objc類(lèi)和值類(lèi)型(struct和enum)動(dòng)態(tài)性两波。
這里舉一個(gè)例子:

struct Test {
    dynamic func test() {}
}

轉(zhuǎn)換成SIL中間碼之后:

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = integer_literal $Builtin.Int32, 0          // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32)        // user: %4
  return %3 : $Int32                              // id: %4
} // end sil function 'main'

// 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.init()
sil hidden [ossa] @$s4main4TestVACycfC : $@convention(method) (@thin Test.Type) -> Test {
bb0(%0 : $@thin Test.Type):
  %1 = alloc_box ${ var Test }, var, name "self"  // user: %2
  %2 = mark_uninitialized [rootself] %1 : ${ var Test } // users: %5, %3
  %3 = project_box %2 : ${ var Test }, 0          // user: %4
  %4 = load [trivial] %3 : $*Test                 // user: %6
  destroy_value %2 : ${ var Test }                // id: %5
  return %4 : $Test                               // id: %6
} // end sil function '$s4main4TestVACycfC'

我們可以看到Test.test()函數(shù)多了一個(gè)dynamically_replacable關(guān)鍵字瞳步, 也就是說(shuō)添加dynamic關(guān)鍵字就是賦予函數(shù)動(dòng)態(tài)替換的能力。關(guān)于這個(gè)關(guān)鍵字腰奋,感興趣的可以看一下這一篇文章单起。

@objc & @nonobjc

@objc 和 @nonobjc 顯式地聲明了一個(gè)函數(shù)是否能被 Objective-C 的運(yùn)行時(shí)捕獲到.
使用 @objc 的典型例子就是給 selector 一個(gè)命名空間 @objc(abc_methodName), 讓這個(gè)函數(shù)可以被 Objective-C 的運(yùn)行時(shí)調(diào)用. 但并不會(huì)改變其派發(fā)方式,依舊是函數(shù)表派發(fā).
@nonobjc 會(huì)改變派發(fā)的方式, 可以用來(lái)禁止消息機(jī)制派發(fā)這個(gè)函數(shù), 不讓這個(gè)函數(shù)注冊(cè)到 Objective-C 的運(yùn)行時(shí)里.
我不確定這跟 final 有什么區(qū)別, 因?yàn)閺氖褂脠?chǎng)景來(lái)說(shuō)也幾乎一樣. 我個(gè)人來(lái)說(shuō)更喜歡 final, 因?yàn)橐鈭D更加明顯.可能final關(guān)鍵字就是@nonobjc的一個(gè)別名吧

我個(gè)人感覺(jué), 這主要是為了跟 Objective-C 兼容用的, final 等原生關(guān)鍵詞, 是讓 Swift 寫(xiě)服務(wù)端之類(lèi)的代碼的時(shí)候可以有原生的關(guān)鍵詞可以使用.

final @objc

可以在標(biāo)記為 final 的同時(shí), 也使用 @objc 來(lái)讓函數(shù)可以使用消息機(jī)制派發(fā).
這么做的結(jié)果就是, 調(diào)用函數(shù)的時(shí)候會(huì)使用直接派發(fā), 但也會(huì)在 Objective-C 的運(yùn)行時(shí)里注冊(cè)響應(yīng)的 selector. 函數(shù)可以響應(yīng) perform(selector:) 以及別的 Objective-C 特性, 但在直接調(diào)用時(shí)又可以有直接派發(fā)的性能.

@inline

Swift 也支持 @inline, 告訴編譯器可以使用直接派發(fā). 但其實(shí)轉(zhuǎn)換成SIL代碼后劣坊,依然是函數(shù)表派發(fā)嘀倒。
有趣的是, dynamic @inline(__always) func dynamicOrDirect() {} 也可以通過(guò)編譯!
但這也只是告訴了編譯器而已, 實(shí)際上這個(gè)函數(shù)還是會(huì)使用消息機(jī)制派發(fā).
這樣的寫(xiě)法看起來(lái)像是一個(gè)未定義的行為, 應(yīng)該避免這么做.

修飾符總結(jié) (Modifier Overview)

關(guān)鍵字 派發(fā)方式
final 直接派發(fā)
static 直接派發(fā)
dynamic 消息機(jī)制派發(fā)
@objc 函數(shù)表派發(fā)
@inline 函數(shù)表派發(fā)

顯式的優(yōu)化 (Visibility Will Optimize)

Swift 會(huì)盡最大能力去優(yōu)化函數(shù)派發(fā)的方式. 例如, 如果你有一個(gè)函數(shù)從來(lái)沒(méi)有 override, Swift 就會(huì)檢測(cè)出并且在可能的情況下使用直接派發(fā).
這個(gè)優(yōu)化大多數(shù)情況下都表現(xiàn)得很好, 但對(duì)于使用了 target / action 模式的 Cocoa 開(kāi)發(fā)者就不那么友好了. 例如:

 override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.rightBarButtonItem = UIBarButtonItem(
        title: "登錄", style: .plain, target: nil,
        action: #selector(ViewController.signInAction)
    )
}
private func signInAction() {}

這里編譯器會(huì)拋出一個(gè)錯(cuò)誤:
Argument of ‘#selector’ refers to a method that is not exposed to Objective-C (Objective-C 無(wú)法獲取 #selector 指定的函數(shù)).
你如果記得 Swift 會(huì)把這個(gè)函數(shù)優(yōu)化為直接派發(fā)的話, 就能理解這件事情了.
這里修復(fù)的方式很簡(jiǎn)單: 加上 @objc 或者 dynamic 就可以保證 Objective-C 的運(yùn)行時(shí)可以獲取到函數(shù).
這種類(lèi)型的錯(cuò)誤也會(huì)發(fā)生在UIAppearance 上, 依賴(lài)于 proxy 和 NSInvocation 的代碼.

另一個(gè)需要注意的是, 如果你沒(méi)有使用 dynamic 修飾的話, 這個(gè)優(yōu)化會(huì)默認(rèn)讓 KVO 失效. 如果一個(gè)屬性綁定了 KVO 的話, 而這個(gè)屬性的 getter 和 setter 會(huì)被優(yōu)化為直接派發(fā), 代碼依舊可以通過(guò)編譯, 不過(guò)動(dòng)態(tài)生成的 KVO 函數(shù)就不會(huì)被觸發(fā).

為什么會(huì)有這些優(yōu)化,可以參考這篇文章

派發(fā)方式總結(jié)

屏幕快照 2020-02-29 下午12.17.27.png

如何選擇派發(fā)方式

使用final關(guān)鍵字修飾肯定不會(huì)被重載的聲明

在上面的文章里局冰,使用 final 可以允許類(lèi)里面的函數(shù)使用直接派發(fā)测蘑。
而 final 關(guān)鍵字可以用在 class, 方法和屬性里來(lái)標(biāo)識(shí)此聲明不可以被 override。
這可以讓編譯器安全的將其優(yōu)化為靜態(tài)派發(fā)康二。

將文件中使用private關(guān)鍵字修飾的聲明推斷為final碳胳。

使用 private 關(guān)鍵字修飾的聲明只能在當(dāng)前文件中進(jìn)行訪問(wèn)。
這樣編譯器可以找到所有潛在的重載聲明赠摇。
任何沒(méi)有被重載的聲明編譯器自動(dòng)的將它推斷為final類(lèi)型并且去除間接的方法調(diào)用和屬性訪問(wèn)固逗。

使用全局模塊優(yōu)化推斷internal聲明為final -> whole module Optimization

使用internal(如果聲明沒(méi)有使用關(guān)鍵詞修飾浅蚪,默認(rèn)是 internal )關(guān)鍵字修飾的聲明的作用域僅限于它被聲明的模塊中。
因?yàn)镾wift通常的將這些文件作為一個(gè)獨(dú)立的模塊進(jìn)行編譯烫罩,所以編譯器不能確定一個(gè)internal聲明有沒(méi)有在其他的文件中被重載惜傲。
然而如果全局模塊優(yōu)化(Whole Module Optimization,關(guān)于全局模塊優(yōu)化參看下文的相關(guān)名詞解釋?zhuān)┦谴蜷_(kāi)的那么所有的模塊將要在同一時(shí)間被一起編譯贝攒。
這樣以來(lái)編譯器就可以為整個(gè)模塊一起做出推斷盗誊,將沒(méi)有被重載的 internal 修飾的聲明推斷為 final 類(lèi)型。

轉(zhuǎn)載自:https://blog.csdn.net/youshaoduo/article/details/103904344

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末隘弊,一起剝皮案震驚了整個(gè)濱河市哈踱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梨熙,老刑警劉巖开镣,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異咽扇,居然都是意外死亡邪财,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)质欲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)树埠,“玉大人,你說(shuō)我怎么就攤上這事嘶伟≡醣铮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵九昧,是天一觀的道長(zhǎng)绊袋。 經(jīng)常有香客問(wèn)我,道長(zhǎng)铸鹰,這世上最難降的妖魔是什么愤炸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮掉奄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凤薛。我一直安慰自己姓建,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布缤苫。 她就那樣靜靜地躺著速兔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪活玲。 梳的紋絲不亂的頭發(fā)上涣狗,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天谍婉,我揣著相機(jī)與錄音,去河邊找鬼镀钓。 笑死穗熬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的丁溅。 我是一名探鬼主播唤蔗,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼窟赏!你這毒婦竟也來(lái)了妓柜?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涯穷,失蹤者是張志新(化名)和其女友劉穎棍掐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體拷况,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡作煌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蝠嘉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片最疆。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蚤告,靈堂內(nèi)的尸體忽然破棺而出努酸,到底是詐尸還是另有隱情,我是刑警寧澤杜恰,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布获诈,位于F島的核電站,受9級(jí)特大地震影響心褐,放射性物質(zhì)發(fā)生泄漏舔涎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一逗爹、第九天 我趴在偏房一處隱蔽的房頂上張望亡嫌。 院中可真熱鬧,春花似錦掘而、人聲如沸挟冠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)知染。三九已至,卻和暖如春斑胜,著一層夾襖步出監(jiān)牢的瞬間控淡,已是汗流浹背嫌吠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掺炭,地道東北人辫诅。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像竹伸,于是被迫代替她去往敵國(guó)和親泥栖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345