Swift 中的動與靜

也許你在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 負責管理各個模塊的 ModelView. 整個頁面以 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")
    }
    
}

testFuncAtestFuncB 雖然都在 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))
    }

}

UITableViewDataSourceOC 中的協議, 使用 Message Dispatch 的派發方式. 而 protocolextension 中定義的方法卻是 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 中:

  • 不得在 extensionoverride 已有的方法.
  • 不得 override extension 里的方法.

所以, 想要達到目的, 還是老老實實的忍住代碼潔癖, 將 customMessage 放到 BaseModuleController 的初始聲明里面去吧╮(╯_╰)╭

后記

此文寫作時并未對 Swift 的代碼實現方式做推敲, 感興趣的讀者請移步 深入理解 Swift 的方法派發.

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容