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é)
如何選擇派發(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