原文: Method Dispatch in Swift
作者: Brain King
譯者: kemchenj
之前看了很多關於 Swift 派發機制的內容, 但覺得沒有一篇可以徹底講清楚這件事情, 看完了這篇文章之後我對 Swift 的派發機制才樹立起了初步的認知.
注釋一張表總結援用類型, 修飾符和它們關於 Swift 函數派發方式的影響.
函數派發就是順序判別運用哪種途徑去調用一個函數的機制. 每次函數被調用時都會被觸發, 但你又不會太留意的一個東西. 理解派發機制關於寫出高功能的代碼來說很有必要, 而且也可以解釋很多 Swift 裡"奇異"的行為.
編譯型言語有三種根底的函數派發方式: 直接派發(Direct Dispatch), 函數表派發(Table Dispatch) 和 音訊機制派發(Message Dispatch), 上面我會細心解說這幾種方式. 大少數言語都會支持一到兩種, Java 默許運用函數表派發, 但你可以經過 final
修飾符修正成直接派發. C++ 默許運用直接派發, 但可以經過加上 virtual
修飾符來改成函數表派發. 而 Objective-C 則總是運用音訊機制派發, 但允許開發者運用 C 直接派發來獲取功能的進步. 這樣的方式十分好, 但也給很多開發者帶來了困擾,
譯者注: 想要理解 Swift 底層構造的人, 極度引薦這段視頻
派發方式 (Types of Dispatch )順序派發的目的是為了通知 CPU 需求被調用的函數在哪裡, 在我們深化 Swift 派發機制之前, 先來理解一下這三種派發方式, 以及每種方式在靜態性和功能之間的取捨.
直接派發 (Direct Dispatch)直接派發是最快的, 不止是由於需求調用的指令集會更少, 並且編譯器還可以有很大的優化空間, 例如函數內聯等, 但這不在這篇博客的討論范圍. 直接派發也有人稱為靜態調用.
但是, 關於編程來說直接調用也是最大的局限, 而且由於缺乏靜態性所以沒方法支持承繼.
函數表派發 (Table Dispatch)函數表派發是編譯型言語完成靜態行為最罕見的完成方式. 函數表運用了一個數組來存儲類聲明的每一個函數的指針. 大局部言語把這個稱為 "virtual table"(虛函數表), Swift 裡稱為 "witness table". 每一個類都會維護一個函數表, 外面記載著類一切的函數, 假如父類函數被 override 的話, 表外面只會保管被 override 之後的函數. 一個子類新添加的函數, 都會被拔出到這個數組的最後. 運轉時會依據這一個表去決議實踐要被調用的函數.
舉個例子, 看看上面兩個類:
class ParentClass {
func method1() {}
func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
func method3() {}
}
在這個狀況下, 編譯器會創立兩個函數表, 一個是 ParentClass
的, 另一個是 ChildClass
的:
這張表展現了 ParentClass 和 ChildClass 虛數表裡 method1, method2, method3 在內存裡的規劃.
let obj = ChildClass()
obj.method2()
當一個函數被調用時, 會閱歷上面的幾個進程:
讀取對象 0xB00
的函數表.
讀取函數指針的索引. 在這裡, method2
的索引是1(偏移量), 也就是 0xB00 + 1
.
跳到 0x222
(函數指針指向 0x222)
查表是一種復雜, 易完成, 而且功能可預知的方式. 但是, 這種派發方式比起直接派發還是慢一點. 從字節碼角度來看, 多了兩次讀和一次跳轉, 由此帶來了功能的損耗. 另一個慢的緣由在於編譯器能夠會由於函數內執行的義務招致無法優化. (假如函數帶有反作用的話)
這種基於數組的完成, 缺陷在於函數表無法拓展. 子類會在虛數函數表的最後拔出新的函數, 沒有地位可以讓 extension 平安地拔出函數. 這篇提案很詳細地描繪了這麼做的局限.
音訊機制派發 (Message Dispatch )音訊機制是調用函數最靜態的方式. 也是 Cocoa 的基石, 這樣的機制催生了 KVO, UIAppearence 和 CoreData 等功用. 這種運作方式的關鍵在於開發者可以在運轉時改動函數的行為. 不止可以經過 swizzling 來改動, 甚至可以用 isa-swizzling 修正對象的承繼關系, 可以在面向對象的根底上完成自定義派發.
舉個例子, 看看上面兩個類:
class ParentClass {
dynamic func method1() {}
dynamic func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
dynamic func method3() {}
}
Swift 會用樹來構建這種承繼關系:
這張圖很好地展現了 Swift 如何運用樹來構建類和子類.
當一個音訊被派發, 運轉時會順著類的承繼關系向上查找應該被調用的函數. 假如你覺得這樣做效率很低, 它的確很低! 但是, 只需緩存樹立了起來, 這個查找進程就會經過緩存來把功能進步到和函數表派發一樣快. 但這只是音訊機制的原理, 這裡有一篇文章很深化的解說了詳細的技術細節.
Swift 的派發機制那麼, 究竟 Swift 是怎樣派發的呢? 我沒能找到一個很長篇大論的答案, 但這裡有四個選擇詳細派發方式的要素存在:
聲明的地位
援用類型
特定的行為
顯式地優化(Visibility Optimizations)
在解釋這些要素之前, 我有必要說清楚, Swift 沒有在文檔裡詳細寫明什麼時分會運用函數表什麼時分運用音訊機制. 獨一的承諾是運用 dynamic
修飾的時分會經過 Objective-C 的運轉時停止音訊機制派發. 上面我寫的一切東西, 都只是我在 Swift 3.0 裡測試出來的後果, 並且很能夠在之後的版本更新裡停止修正.
在 Swift 裡, 一個函數有兩個可以聲明的地位: 類型聲明的作用域, 和 extension. 依據聲明類型的不同, 也會有不同的派發方式.
class MyClass {
func mainMethod() {}
}
extension MyClass {
func extensionMethod() {}
}
下面的例子裡, mainMethod
會運用函數表派發, 而 extensionMethod
則會運用直接派發. 當我第一次發現這件事情的時分覺得很不測, 直覺上這兩個函數的聲明方式並沒有那麼大的差別. 上面是我依據類型, 聲明地位總結出來的函數派發方式的表格.
這張表格展現了默許狀況下 Swift 運用的派發方式.
總結起來有這麼幾點:
值類型總是會運用直接派發, 復雜易懂
而協議和類的 extension 都會運用直接派發
NSObject
的 extension 會運用音訊機制停止派發
NSObject
聲明作用域裡的函數都會運用函數表停止派發.
協議裡聲明的, 並且帶有默許完成的函數會運用函數表停止派發
援用類型 (Reference Type Matters)援用的類型決議了派發的方式. 這很不言而喻, 但也是決議性的差別. 一個比擬罕見的疑惑, 發作在一個協議拓展和類型拓展同時完成了同一個函數的時分.
protocol MyProtocol {
}
struct MyStruct: MyProtocol {
}
extension MyStruct {
func extensionMethod() {
print("構造體")
}
}
extension MyProtocol {
func extensionMethod() {
print("協議")
}
}
let myStruct = MyStruct()
let proto: MyProtocol = myStruct
myStruct.extensionMethod() // -> “構造體”
proto.extensionMethod() // -> “協議”
剛接觸 Swift 的人能夠會以為 proto.extensionMethod()
調用的是構造體裡的完成. 但是, 援用的類型決議了派發的方式, 協議拓展裡的函數會運用直接調用. 假如把 extensionMethod
的聲明挪動到協議的聲明地位的話, 則會運用函數表派發, 最終就會調用構造體裡的完成. 並且要記得, 假如兩種聲明方式都運用了直接派發的話, 基於直接派發的運作方式, 我們不能夠完成料想的 override
行為. 這關於很多從 Objective-C 過渡過去的開發者是反直覺的.
Swift JIRA(缺陷跟蹤管理零碎) 也發現了幾個 bugs, Swfit-Evolution 郵件列表裡有一大堆討論, 也有一大堆博客討論過這個. 但是, 這仿佛是成心這麼做的, 雖然官方文檔沒有提過這件事情
指定派發方式 (Specifying Dispatch Behavior)Swift 有一些修飾符可以指定派發方式.
finalfinal
允許類外面的函數運用直接派發. 這個修飾符會讓函數得到靜態性. 任何函數都可以運用這個修飾符, 就算是 extension 裡原本就是直接派發的函數. 這也會讓 Objective-C 的運轉時獲取不到這個函數, 不會生成相應的 selector.
dynamic
可以讓類外面的函數運用音訊機制派發. 運用 dynamic
, 必需導入 Foundation
框架, 外面包括了 NSObject
和 Objective-C 的運轉時. dynamic
可以讓聲明在 extension 外面的函數可以被 override. dynamic
可以用在一切 NSObject
的子類和 Swift 的原聲類.
@objc
和 @nonobjc
顯式地聲明了一個函數能否能被 Objective-C 的運轉時捕捉到. 運用 @objc
的典型例子就是給 selector 一個命名空間 @objc(abc_methodName)
, 讓這個函數可以被 Objective-C 的運轉時調用. @nonobjc
會改動派發的方式, 可以用來制止音訊機制派發這個函數, 不讓這個函數注冊到 Objective-C 的運轉時裡. 我不確定這跟 final
有什麼區別, 由於從運用場景來說也簡直一樣. 我團體來說更喜歡 final
, 由於意圖愈加分明.
譯者注: 我團體覺得, 這這次要是為了跟 Objective-C 兼容用的, final
等原生關鍵詞, 是讓 Swift 寫服務端之類的代碼的時分可以有原生的關鍵詞可以運用.
可以在標志為 final
的同時, 也運用 @objc
來讓函數可以運用音訊機制派發. 這麼做的後果就是, 調用函數的時分會運用直接派發, 但也會在 Objective-C 的運轉時裡注冊呼應的 selector. 函數可以呼應 perform(selector:)
以及別的 Objective-C 特性, 但在直接調用時又可以有直接派發的功能.
Swift 也支持 @inline
, 通知編譯器可以運用直接派發. 風趣的是, dynamic @inline(__always) func dynamicOrDirect() {}
也可以經過編譯! 但這也只是通知了編譯器而已, 實踐上這個函數還是會運用音訊機制派發. 這樣的寫法看起來像是一個未定義的行為, 應該防止這麼做.
這張圖總結這些修飾符關於 Swift 派發方式的影響.
假如你想檢查下面一切例子的話, 請看這裡.
可見的都會被優化 (Visibility Will Optimize)Swift 會盡最大才能去優化函數派發的方式. 例如, 假如你有一個函數歷來沒有 override, Swift 就會檢車並且在能夠的狀況下運用直接派發. 這個優化大少數狀況下都表現得很好, 但關於運用了 target / action 形式的 Cocoa 開發者就不那麼敵對了. 例如:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "登錄", style: .plain, target: nil,
action: #selector(ViewController.signInAction)
)
}
private func signInAction() {}
這裡編譯器會拋出一個錯誤: Argument of '#selector' refers to a method that is not exposed to Objective-C (Objective-C 無法獲取 #selector 指定的函數)
. 你假如記得 Swift 會把這個函數優化為直接派發的話, 就能了解這件事情了. 這裡修復的方式很復雜: 加上 @objc
或許 dynamic
就可以保證 Objective-C 的運轉時可以獲取到函數了. 這品種型的錯誤也會發作在UIAppearance
上, 依賴於 proxy 和 NSInvocation
的代碼.
另一個需求留意的是, 假如你沒有運用 dynamic
修飾的話, 這個優化會默許讓 KVO 生效. 假如一個屬性綁定了 KVO 的話, 而這個屬性的 getter 和 setter 會被優化為直接派發, 代碼照舊可以經過編譯, 不過靜態生成的 KVO 函數就不會被觸發.
Swift 的博客有一篇很贊的文章描繪了相關的細節, 和這些優化面前的思索.
派發總結 (Dispatch Summary)這裡有一大堆規則要記住, 所以我整理了一個表格:
這張表總結援用類型, 修飾符和它們關於 Swift 函數派發的影響
NSObject 以及靜態性的損失 (NSObject and the Loss of Dynamic Behavior)不久之前還有一群 Cocoa 開發者討論靜態行為帶來的問題. 這段討論很風趣, 提了一大堆不同的觀念. 我希望可以在這裡持續討論一下, 有幾個 Swift 的派發方式我覺得損害了靜態性, 特地說一下我的處理方案.
NSObject 的函數表派發 (Table Dispatch in NSObject)下面, 我提到 NSObject
子類定義裡的函數會運用函數表派發. 但我覺得很迷惑, 很難解釋清楚, 並且由於上面幾個緣由, 這也只帶來了一點點功能的提升:
大局部 NSObject
的子類都是在 obj_msgSend
的根底上構建的. 我很疑心這些派發方式的優化, 實踐究竟會給 Cocoa 的子類帶來多大的提升.
大少數 Swift 的 NSObject
子類都會運用 extension 停止拓展, 都沒方法運用這種優化.
最後, 有一些小細節會讓派發方式變得很復雜.
派發方式的優化毀壞了 NSObject 的功用 (Dispatch Upgrades Breaking NSObject Features)功能提升很棒, 我很喜歡 Swift 關於派發方式的優化. 但是, UIView
子類顏色的屬性實際上功能的提升毀壞了 UIKit 現有的形式.
原文: However, having a theoretical performance boost in my UIView
subclass color property breaking an established pattern in UIKit is damaging to the language.
運用靜態派發的話構造體是個不錯的選擇, 而運用音訊機制派發的話則可以思索 NSObject
. 如今, 假如你想跟一個剛學 Swift 的開發者解釋為什麼某個東西是一個 NSObject
的子類, 你不得不去引見 Objective-C 以及這段歷史. 如今沒有任何理由去承繼 NSObject
構建類, 除非你需求運用 Objective-C 構建的框架.
目前, NSObject
在 Swift 裡的派發方式, 一句話總結就是復雜, 跟理想還是有差距. 我比擬想看到這個修正: 當你承繼 NSObject
的時分, 這是一個你想要完全運用靜態音訊機制的表現.
另一個 Swift 可以改良的中央就是函數靜態性的檢測. 我覺得在檢測到一個函數被 #selector
和 #keypath
援用時要自動把這些函數標志為 dynamic
, 這樣的話就會處理大局部 UIAppearance
的靜態問題, 但也許有別的編譯時的處置方式可以標志這些函數.
為了讓我們對 Swift 的派發方式有更多理解, 讓我們來看一下 Swift 開發者遇到過的 error.
SR-584這個 Swift bug 是 Swift 函數派發的一個功用. 存在於 NSObject
子類聲明的函數(函數表派發), 以及聲明在 extension 的函數(音訊機制派發)中. 為了更好地描繪這個狀況, 我們先來創立一個類:
class Person: NSObject {
func sayHi() {
print("Hello")
}
}
func greetings(person: Person) {
person.sayHi()
}
greetings(person: Person()) // prints 'Hello'
greetings(person:)
函數運用函數表派發來調用 sayHi()
. 好像我們看到的, 希冀的, "Hello" 會被打印. 沒什麼好講的中央, 那如今讓我們承繼 Persion
:
class MisunderstoodPerson: Person {}
extension MisunderstoodPerson {
override func sayHi() {
print("No one gets me.")
}
}
greetings(person: MisunderstoodPerson()) // prints 'Hello'
可以看到, sayHi()
函數是在 extension 裡聲明的, 會運用音訊機制停止調用. 當greetings(person:)
被觸發時, sayHi()
會經過函數表被派發到 Person
對象, 而misunderstoodPerson
重寫之後會是用音訊機制, 而 MisunderstoodPerson
的函數表照舊保存了 Person
的完成, 緊接著歧義就發生了.
在這裡的處理辦法是保證函數運用相反的音訊派發機制. 你可以給函數加上 dynamic
修飾符, 或許是把函數的完成從 extension 挪動到類最初聲明的作用域裡.
了解了 Swift 的派發方式, 就可以了解這個行為發生的緣由了, 雖然 Swift 不應該讓我們遇到這個問題.
SR-103這個 Swift bug 觸發了定義在協議拓展的默許完成, 即便是子類曾經完成這個函數的狀況下. 為了闡明這個問題, 我們先定義一個協議, 並且給外面的函數一個默許完成:
protocol Greetable {
func sayHi()
}
extension Greetable {
func sayHi() {
print("Hello")
}
}
func greetings(greeter: Greetable) {
greeter.sayHi()
}
如今, 讓我們定義一個恪守了這個協議的類. 先定義一個 Person
類, 恪守 Greetable
協議, 然後定義一個子類 LoudPerson
, 重寫 sayHi()
辦法.
class Person: Greetable {
}
class LoudPerson: Person {
func sayHi() {
print("HELLO")
}
}
你們發現 LoudPerson
完成的函數後面沒有 override
修飾, 這是一個提示, 也許代碼不會像我們想象的那樣運轉. 在這個例子裡, LoudPerson
沒有在 Greetable
的協議記載表(Protocol Witness Table)裡成功注冊, 當 sayHi()
經過 Greetable
協議派發時, 默許的完成就會被調用.
處理的辦法就是, 在類聲明的作用域裡就要提供一切協議裡定義的函數, 即便曾經有默許完成. 或許, 你可以在類的後面加上一個 final
修飾符, 保證這個類不會被承繼.
Doug Gregor 在 Swift-Evolution 郵件列表裡提到, 經過顯式地重新把函數聲明為類的函數, 就可以處理這個問題, 並且不會偏離我們的想象.
其它 bug (Other bugs)Another bug that I thought I’d mention is SR-435. It involves two protocol extensions, where one extension is more specific than the other. The example in the bug shows one un-constrained extension, and one extension that is constrained to Equatable
types. When the method is invoked inside a protocol, the more specific method is not called. I’m not sure if this always occurs or not, but seems important to keep an eye on.
另外一個 bug 我在 SR-435 裡曾經提過了. 當有兩個協議拓展, 而其中一個愈加詳細時就會觸發. 例如, 有一個不受約束的 extension, 而另一個被 Equatable
約束, 當這個辦法經過協議派發, 約束比擬多的那個 extension 的完成則不會被調用. 我不太確定這是不是百分之百能復現, 但有必要留個心眼.
If you are aware of any other Swift dispatch bugs, drop me a line and I’ll update this blog post.
假如你發現了其它 Swift 派發的 bug 的話, @一下我我就會更新到這篇博客裡.
風趣的 Error (Interesting Error)有一個很好玩的編譯錯誤, 可以窺見到 Swift 的方案. 好像之前說的, 類拓展運用直接派發, 所以你試圖 override 一個聲明在 extension 裡的函數的時分會發作什麼?
class MyClass {
}
extension MyClass {
func extensionMethod() {}
}
class SubClass: MyClass {
override func extensionMethod() {}
}
下面的代碼會觸發一個編譯錯誤 Declarations in extensions can not be overridden yet
(聲明在 extension 裡的辦法不可以被重寫). 這能夠是 Swift 團隊計劃增強函數表派發的一個征兆. 又或許這只是我過度解讀, 覺得這門言語可以優化的中央.
我希望理解函數派發機制的進程中你感遭到了樂趣, 並且可以協助你更好的了解 Swift. 雖然我埋怨了 NSObject
相關的一些東西, 但我還是覺得 Swift 提供了高功能的能夠性, 我只是希望可以有足夠復雜的方式, 讓這篇博客沒有存在的必要.
以上就是對深化了解 Swift 派發機制的相關引見,希望對您學習IOS有所協助,感激您關注本站!
【深化了解 Swift 派發機制】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!