共计 3424 个字符,预计需要花费 9 分钟才能阅读完成。
派发(dispatch)是一个比拟通用的概念,个别是指为了实现某个目标把一个货色发送到某个地位的行为。在计算机科学中,这个术语在很多中央都会用到,比方派发一个调用给某个函数,派发一个事件给一个监听者,派发一个中断给中断处理程序,或者派发一个过程给 CPU。
在这篇文章中,咱们次要钻研 Swift 中的派发,也就是派发一个调用到某个办法上,Swift 中的办法派发包含类的办法派发和基于协定的派发。
类的办法派发
Swift 中类的办法的派发有以下三种形式:
- 动态派发(Static Dispatch)
- 动静派发(Dynamic Dispatch)
- 音讯派发(Messaging Dispatch)
动态派发
动态派发,又叫做晚期绑定,是指在编译期将办法调用绑定到办法的实现上,这种派发形式十分快。在编译期,编译器能够看到调用方和被调方的所有信息,间接生成跳转代码,这样在运行期就不会有其它额定的开销。并且编译器能够依据本人晓得的信息进行优化,比方内联,能够极大进步程序运行效率。
在 Swift 中,构造体和枚举的办法调用,以及被 final
标记的类和类的办法,都会采纳这种派发形式。
动静派发
动静派发是在运行时决定办法调用地址,因而须要有个查找办法地址的机制,在 Swift 中是通过 虚函数表(Virtual Method Table),简称 V-Table 实现的,因而动静派发也被称为表派发(Table Dispatch)
在编译期,编译器会给每个蕴含动静派发办法的类型创立一个虚函数表,这个表会被放在内存的动态区,表中是办法名到办法实现地址的映射。当这个类型的办法被调用时,运行时会去这个类型的虚函数表中寻找这个办法名对应的实现地址,而后再跳转到这个地址执行代码。
动静派发次要是用来实现 继承多态,继承多态是多态的一种。例如以下代码:
class Animal {func makeNoise() {fatalError("此办法必须通过子类调用")
}
}
class Dog: Animal {override func makeNoise() {print("Wang Wang!")
}
}
class Cat: Animal {override func makeNoise() {print("Miao!")
}
}
这段代码在编译时,编译器会把 makeNoise 办法采纳动静派发来解决,会给 Animal、Dog、Cat 这三个类别离生成一个虚函数表,每个表中蕴含了办法实现地址的列表和办法列表的索引。
咱们能够应用一个容器来装一些列 Animal 和其子类,而后对立调用 makeNoise 办法,这样的益处是疏忽每个具体类型的信息,提供高级的形象,这种做法在很多中央都很有用。这种做法在面向对象中也被称为凋谢递归(Open recursion)。
let animals: [Animal] = [Dog(), Cat()]
for animal in animals {animal.makeNoise()
}
// 输入:// Wang Wang!
// Miao!
绝对于动态派发的间接跳转,动静派发要通过 3 个步骤,找到虚函数表、找到办法地址、跳转到办法地址,并且编译器无奈对动静派发做优化,因而其性能要比动态派发慢得多。
重写扩大中的办法
Swift 中扩大中的办法是不能被子类重写的,能够尝试编写以下代码:
extension Animal {func methodInExtension() {}}
class Dog: Animal {
...
override func methodInExtension() {}
}
此时 Xcode 会报告一个编译谬误 扩大中的非 @objc 的实例办法不能被重写
。这是因为扩大中的办法不会被增加到类的虚函数表中。如果肯定想重写办法,只能增加 @objc
修饰符,这样这个办法会领有残缺的 Objective-C 的办法派发能力,编译器会晓得这个办法能够在运行时被正确处理,从而容许重写。
默认状况下,如果继承了一个 Objective-C 类,子类中的办法派发是采纳动静派发而不是音讯派发。
音讯派发
对于音讯派发,这就是 Objective-C 的常识了,就是 OC 运行时通过 isa 和 super 指针查找办法实现,并蕴含一系列音讯转发流程,在此不表。
在 Swift 类中应用 @objc dynamic
关键字能够强制办法应用音讯派发。
协定的派发
类继承是一个很好用的货色,然而它也存在一些问题,比方子类只能继承一个父类,并且子类会被强制蕴含父类的内存布局。
Swift 提供了一个解决方案来解决上述类继承的有余,这个解决方案提供了良好的封装,反对多态,不会和某个特定的内存布局绑定,并且能够基于值类型工作,这就是利用 面向协定编程(POP)。
协定定义了一个类型具备的能力,和继承不同,咱们能够给让一个类型合乎任意多个协定,能够让不是本人写的类型去合乎一个协定,能够给协定提供默认实现。在 Swift 中,类、构造体、枚举都能够去合乎协定。
用面向协定的思维来编程,咱们就会摒弃类继承,而是从设计一个协定开始,比方下面的代码,咱们会将 Animal 设计为一个协定:
protocol Animal {func makeNoise()
}
而后能够用一个协定类型的变量来保留一个对象:
let animal: Animal = ...
在类继承中,因为 Animal 是一个类,编译器晓得 Animal 占用多大的内存空间,因而晓得 animal 对象应该占用多大空间,然而如果 Animal 是一个协定类型,编译器怎么晓得 animal 应该占用多大空间呢?
class Dog: Animal {
let name: String
func makeNoise() { ...}
}
class Cat: Animal {
let age: Int
func makeNoise() { ...}
}
协定并不限度合乎协定的类型的内存布局,下面代码中,Dog 占 3 个字的大小,Cat 占 1 个字的大小。
Swift 引入了 存在容器(Existential Container) 来解决这个问题。每个存在容器由以下几个局部组成:
- Value Buffer ValueBuffer 占 3 个字的长度,如果合乎协定的对象是值类型且小于等于 3 个字,则间接放入 ValueBuffer 中,如果对象是援用类型或者大于 3 个字的值类型,则将对象放在堆上,在 ValueBuffer 中保留一个指向堆上对象的援用。
- 一个指向 值目睹表(Value Witness Table, VWT) 的指针,用来创立、拷贝和销毁值,表中保留了创立、拷贝、销毁等函数的地址,其中创立、销毁函数的地址仅在当对象调配在堆上时才会有。
- 一个指向 协定目睹表(Protocol Witness Table, PWT) 的指针,每个合乎了某个协定的类型都有本人的协定目睹表,保留了实现协定中办法的办法地址。
- 如果类型合乎了多个协定,前面还会有第二个协定的协定目睹表指针,以及第三个,第四个等。合乎的协定越多,存在容器占用内存空间就越大。
这样对于某个协定类型,它的存在容器的大小总是雷同的,编译器即可确定它的大小。
let animal: Animal = Dog()
animal.makeNoise()
下面的代码,animal 会被解决成一个存在容器,占用 5 个字大小的空间,因为 Dog 的大小小于等于 3 个字,它被间接放入存在容器的 ValueBuffer 中,也就是头 3 个字的空间。第 4 个字的地位是 VWT,保留了对象拷贝等函数的地址。在 PWT 中保留了 makeNoise 办法的实现地址,用存在容器第 5 个字的地位指向 PWT。
当调用 makeNoise 时,运行时会去 PWT 中寻找办法的地址,而后跳转指令,这其实和虚函数表差不多。
值得一提的是,应用协定类型的开销可能会很大,尤其是实现协定的对象是比拟大的对象的时候,这会导致在堆上进行调配和援用技术操作。这种状况下应用泛型束缚可能是更好的抉择。
总结
了解了 Swift 中的办法派发形式后,能够晓得,应该优先应用动态派发,能够获得最佳的性能,只有在须要和 Objective-C 代码交互时才应该应用音讯派发。在须要动静派发的中央,应该优先应用面向协定设计应用基于协定的派发,而后依据具体情况应用类自身的动静派发。
参考资料
- https://developer.apple.com/w…
- Swift. Method Dispatch | by Maxim Krylov | Medium
- Understanding method dispatch in Swift | by Navdeep Singh | Heartbeat (comet.ml)