也許你在swift 使用過程中永遠也不會遇到這些奇怪的行為, 但是進來看看又不要錢~
Swift 相比于其它語言有一個很好的特性, 開發者不僅可以給協議增加接口, 還能進一步給協議提供方法實現. 通過這個 Feature, 開發者可以使用組合而非繼承的思想來設計對象, 也就是所謂的面向接口編程.
但是需要明確的是, 這個特性并不是可以隨心所欲的加以使用. 在日常 coding 中偶爾能遇到某個行為被編譯器報錯, 又或者編譯通過后表現出預期外行為. 這篇文章整理了我在開發過程中遇到的一些問題, 并通過 Swift 的派發機制來解釋這些行為背后的原因.
從一個小問題講起
protocol MyProtocol {
func testFuncA()
}
extension MyProtocol {
func testFuncA() {
print("MyProtocol's testFuncA")
}
func testFuncB() {
print("MyProtocol's testFuncB")
}
}
看到上面的例子, 我們定義了一個協議 MyProtocol, 該協議存在一個必須實現的方法 testFuncA.
此外, 我們通過擴展為改協議提供 testFuncA 的默認實現, 并額外的提供了一個名為 testFuncB 方法的默認實現.
class MyClass: MyProtocol {
func testFuncA() {
print("MyClass's testFuncA")
}
func testFuncB() {
print("MyClass's testFuncB")
}
}
接著定義一個類 MyClass, 該類提供 testFuncA, testFuncA 的方法實現.
那么問題來了:
let object: MyProtocol = MyClass()
object.testFuncA()
object.testFuncB()
對聲明為協議類型的對象分別調用 testFuncA, testFuncA, 實際調用的方法究竟是 MyClass 提供的呢, 還是 MyProtocol 提供的默認實現?
Output:
MyClass's testFuncA
MyProtocol's testFuncB
可以看到, 調用 MyProtocol 內聲明的方法時, 最終調用到了 MyClass 內部的實現, 而調用方法未在協議內聲明時, 實際調用到了協議擴展中提供的實現.
這個問題并不罕見, 開發者可能已經見怪不怪. 不過莫急, 在解釋背后的原因之前, 我還想拋出兩個問題.
TableView 轉發器
這是我在開發中遇到的一個問題.
隨著版本迭代, 一些頁面里的內容越來越多. 為了避免 Massive View Controller 問題, 我將頁面中的內容劃分為一個個的 Module, Module 負責管理各個模塊的 Model 和 View. 整個頁面以 TableView 的形式組織, 由最外面的容器 ViewController 轉發 TableView 的數據源代理方法到各個 Module 中去.
出于面向協議的設計思路, 首先想到的是將上述黑體字所描述的功能通過協議擴展的方式實現, 這樣一來對象只需要遵循我所設計的協議就可以獲得**轉發 TableView 的數據源代理方法到各個 Module **的功能.
extension ModuleContainerProtocol where Self: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return modules.reduce(0) { $0 + $0.numberOfSections?(in: self.tableView) ?? 1 }
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let (module, section) = convertToTargetModule(with: indexPath.section)
return module.tableView(tableView, cellForRowAt: IndexPath(row: indexPath.row, section: section))
}
}
方法里的實現是往各個 Module 轉發消息的實現, 可以忽略.
問題是: 可以在 Swift 的協議擴展里為 Objective-C 的協議提供方法實現嗎?
Module 通信
這個同樣是在開發 Module 過程中遇到的問題.
在某些業務場景下需要 Module 向主容器發送一些特定的業務消息, 主容器以遵循 ModuleContainer 協議的泛型的形式被 Module 弱引用.
我為 ModuleContainer 添加一個方法, Module 通過這個方法可以向主容器發送特定的業務消息. 這個方法從功能上看明顯是可選的, 但是我不想添加 optional 關鍵字(還得將協議聲明為 @objc, 成本較大), 于是我想了一個"巧妙"的方法:
protocol ModuleContainer {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?)
}
extension ModuleContainer {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {}
}
// 該類實現 TableView 轉發器功能
class BaseModuleController: UIViewController, ModuleContainer {
}
class MyViewController: BaseModuleController {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {
// 接受消息并處理
}
}
在為 ModuleContainer 聲明方法的同時, 在協議擴展里為其提供默認實現.
在需要該方法的業務層, 比如 MyViewController 中, 再覆蓋該方法的默認實現. 或許你已經發現了, 缺少 override 關鍵字也能編譯通過.
let moduleContainer: ModuleCOntainer
moduleContainer.customMessage(someKey, parameters: nil)
那么問題來了, 與最開始的一個例子一樣, customMessage 是聲明在協議內部的, 差別在于提供覆蓋實現的類并不是遵循這個協議的類而是該類的子類. 這樣的調用方式最終能否調用到 MyViewController 提供的實現呢?
劇透一下, 不能, 原因稍后再講.
在這個基礎上我又做了一些改進, 我在 BaseModuleController 里提供協議的實現, 在 MyViewController 中再覆蓋其父類的實現, 這樣終于能見到熟悉的 override 關鍵字了.
// 該類實現 TableView 轉發器功能
class BaseModuleController: UIViewController, ModuleContainer {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {
// Inspired by subclass if needed.
}
}
class MyViewController: BaseModuleController {
override func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {
// 接受消息并處理
}
}
最后一個問題, 這樣能達到目的嗎?
在解釋問題之前, 首先簡短的介紹一下各種語言常見的三種函數派發方式.
Direct, Table & Message
不少 Swift 開發人員都有過 Objective-C 的開發經歷, 而 Objective-C 最讓人印象深刻的, 就是那奇怪的語法和基于消息的函數派發方式了.
Message
基于消息是最為靈活的一種派發方式, 最大限度的允許開發者在運行時修改函數的行為.
以 Objective-C 為例, 所有對象都擁有一個 isa 指針, 可以通過該指針找到對應的方法列表. 方法列表中存儲著該類實現的方法(不包括父類實現的方法)以及指向父類方法列表的指針. 當消息派發時, 會沿著類的方法列表到父類的方法列表(Super 指針)的順序尋找方法實現.
在 Swift 中, Dynamic 關鍵字可以為方法加上運行時特性.
class MySuperClass {
dynamic func testFuncA() {}
dynamic func testFuncB() {}
}
class MyClass: MySuperClass {
override func testFuncB() {}
dynamic func testFuncC() {}
}
MyClass | MySuperClass |
---|---|
Super | testFuncA |
testFuncB(New) | testFuncB |
testFuncC |
Table
函數表是最為常見的函數派發方式.
同 Message Dispatch 類似, 所有類也會維護一個自己的函數表, 不同的是所有未被復寫的父類所實現的函數地址都會拷貝在這個表中, 而不是由一個指向父類方法表的指針替代. 由于少了一步指針尋址步驟, 在派發效率上要比基于消息的派發高效, 但是在靈活性上打了折扣.
在 Swift 中, 該表被稱為 Witness Table.
class MySuperClass {
func testFuncA() {}
func testFuncB() {}
}
class MyClass: MySuperClass {
override func testFuncB() {}
func testFuncC() {}
}
MyClass | MySuperClass |
---|---|
testFuncA | testFuncA |
testFuncB(New) | testFuncB |
testFuncC |
Direct
直接派發是效率最高的, 在編譯階段就能確定調用的函數地址. 但是缺乏了動態特性, 也不支持繼承.
In Swift
Swift 支持了全部三種派發方式, 根據具體的使用場景和關鍵字決定派發方式.
下面是搜集到的一位開發者整理的 Swift3 下派發方式的測試結果. 之后我會將 Swift4 下的測試結果更新在這里.
Initial Declaration | Extension | |
---|---|---|
Value Type | Direct | Direct |
Protocol | Table | Direct |
Class | Table | Direct |
NSObject SubClass | Table | Message |
以及該位開發者在 Swift3 下的總結:
- 值類型總是會使用直接派發, 簡單易懂
- 而協議和類的 extension 都會使用直接派發
-
NSObject
的 extension 會使用消息機制進行派發 -
NSObject
聲明作用域里的函數都會使用函數表進行派發. - 協議里聲明的, 并且帶有默認實現的函數會使用函數表進行派發
需要注意的是, 盡管開發者文檔表明了部分場景的派發情況, 但是實際的派發方式可能會被優化(Increasing Performance by Reducing Dynamic Dispatch). 比如 @objc 關鍵字能為方法添加運行時特性, 但是在使用的時候仍有可能被優化成 static dispatch. 且除了 final,private 一些關鍵字能讓編譯器在編譯期就能確定調用的函數地址外, Whole Module Optimization 選項能讓絕大多數未被重寫的方法得到編譯器的優化.
回到問題
一個小問題
protocol MyProtocol {
func testFuncA()
}
extension MyProtocol {
func testFuncA() {
print("MyProtocol's testFuncA")
}
func testFuncB() {
print("MyProtocol's testFuncB")
}
}
testFuncA 和 testFuncB 雖然都在 MyProtocol 的擴展中提供了默認實現, 但是:
- testFuncA 的默認實現注冊到了 MyProtocol 的函數表中.
- testFuncB 的函數實現將會被直接派發.
let object: MyProtocol = MyClass()
object.testFuncA()
object.testFuncB()
因此, MyClass 在實現 testFuncA 的時候, 也將這個實現注冊到了 MyProtocol 的函數表中. 在調用 testFuncA 的時候, 會在函數表中查找對應的實現. 而在編譯的時候就已經將 MyProtocol 中關于 testFuncB 的函數地址作為派發的地址給確定下來了, 根本不關心 object 的具體類型.
因此在調用的時候 testFuncA 使用的是 MyClass 的實現, 而 testFuncB 使用的是 MyProtocol 的實現.
TableView 轉發器
extension ModuleContainerProtocol where Self: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return modules.reduce(0) { $0 + $0.numberOfSections?(in: self.tableView) ?? 1 }
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let (module, section) = convertToTargetModule(with: indexPath.section)
return module.tableView(tableView, cellForRowAt: IndexPath(row: indexPath.row, section: section))
}
}
UITableViewDataSource 是OC 中的協議, 使用 Message Dispatch 的派發方式. 而 protocol 的 extension 中定義的方法卻是 objc_msgSend() 方法不可見的, 因此在編譯器會報錯.
Non-'@objc' method 'tableView(_:numberOfRowsInSection:)' does not satisfy requirement of '@objc' protocol 'UITableViewDataSource'
既然是對 OC 不可見, 加上 @objc 關鍵字能否滿足需求呢?
@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes
很遺憾, 目前 Swift 不支持這種操作, 不過不排除未來支持的可能性, 參見Non-'@objc' method does not satisfy optional requirement of '@objc' protocol.
同理, 直接為 UITableViewDataSource 為原方法提供默認實現也是不可取的, 添加的方法在 Swift 側可以調用, 但是在 OC 測(也就是 UIKit 內的實現) 是完全不可見這個方法的.
Module 通信
protocol ModuleContainer {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?)
}
extension ModuleContainer {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {}
}
// 該類實現 TableView 轉發器功能
class BaseModuleController: UIViewController, ModuleContainer {
}
class MyViewController: BaseModuleController {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {
// 接受消息并處理
}
}
customMessage 方法由于聲明在 ModuleContainer 的原始定義內, 因此派發方式為 Table Dispatch. 需要注意的是, 在 BaseModuleController 內提供的 customMessage 實現才會注冊進 ModuleContainer 的函數表, 也就是說 MyViewController 內的 CustomMessage 方法并不在函數表中.
let moduleContainer: ModuleCOntainer
moduleContainer.customMessage(someKey, parameters: nil)
所以這樣調用的結果自然是會調用到 ModuleContainer 所提供的 customMessage 默認實現了.
one more thing
// 該類實現 TableView 轉發器功能
class BaseModuleController: UIViewController, ModuleContainer {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {
// Inspired by subclass if needed.
}
}
class MyViewController: BaseModuleController {
override func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {
// 接受消息并處理
}
}
經過修改后, BaseModuleController 提供了 customMessage 的實現并注冊到了 ModuleContainer 的函數表里.
然后通過 override 關鍵字實現再往 ModuleContainer 的函數表里添加一個實現?
答案是可以的, 但是需要注意的是, 一般開發者習慣將遵循某個協議的方法單獨卸載 extension 中, 使得代碼分布更加清晰.
extension BaseModuleController: ModuleContainer {
func customMessage(_ key: ModuleContainerCustomKey, parameters: [Any]?) {
// Inspired by subclass if needed.
}
}
但是在 swift 中:
- 不得在 extension 中 override 已有的方法.
- 不得 override extension 里的方法.
所以, 想要達到目的, 還是老老實實的忍住代碼潔癖, 將 customMessage 放到 BaseModuleController 的初始聲明里面去吧╮(╯_╰)╭
后記
此文寫作時并未對 Swift 的代碼實現方式做推敲, 感興趣的讀者請移步 深入理解 Swift 的方法派發.