也许你在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 的方法派发.
参考资料
- what-does-the-dynamic-keyword-mean-in-swift-3
- Increasing Performance by Reducing Dynamic Dispatch
- Method Dispatch in Swift
- Non-'@objc' method does not satisfy optional requirement of '@objc' protocol