10、理解Swift中方法的派发机制
派发机制
函数/方法把代码内聚到一处并对外暴露函数名,这提高了代码的复用性,也对外隐藏了具体的实现过程。根据函数名找到具体的函数实现,这就是函数派发的过程。函数派发的机制分两种:静态派发和动态派发。
静态派发
静态派发机制下,“方法的实现在编译期就已确定”,即编译器在编译期就已经能确定函数具体实现的位置在哪。调用函数时,runtime 会直接跳转到函数的内存地址上执行具体的实现。
优点:执行快、性能好、编译器能进行内联等优化。
缺点:缺乏动态性,函数实现在运行期不能修改,无法满足某些特定的需求,比如在运行时替换某个方法。
动态派发
动态派发,是指“在运行时决定方法调用哪个实现”的过程。动态派发机制产生的原因是面向对象语言的多态性。动态派发机制下,编译器在编译期还不知道函数的具体实现是哪个;在执行函数时runtime才会根据函数名去函数表中查找并执行具体的实现。每种语言都有自己的机制来支持动态派发,例如swift支持函数表派发、消息派发~
缺点:需要查表,执行效率相对低一些。
派发机制的目的是为了让程序告诉CPU,当调用一个具体方法的时候要去内存的哪个地方找到可执行代码。
1、Swift中有哪些派发方法
由于Swift是一门集成了多种编程范式的语言,面向对象的、面向protocol的,面向过程的。为了支持这些编程范式,Swift采用了几乎所有常用的方法派发方式。
编译型语言有三种函数派发方式:直接派发(Direct Dispatch)、函数表派发(Table Dispatch)和消息机制派发(Message Dispatch)。大多数编程语言都会支持一到两种:
(1) Java默认使用函数表派发,但是你可以通过final修饰符修改成直接派发;
(2) C++默认是使用直接派发,但是可以通过加上virtual修饰符来改成函数表派发;
(3) OC则是使用消息机制派发,但是允许开发者使用C直接派发来获取性能的提高。
1.1 Direct Dispatch(直接派发)
直接派发发生在编译阶段,根据调用者声明的类型,到这个类型中去找方法的实现。直接派发时最快的,直接派发缺乏动态性,没有办法支持继承。
直接派发就是程序字面上调用什么方法,就生成调用对应方法的代码。
struct MyStruct {
func myMethod1() {
let number = 10
}
}
let myStruct = MyStruct()
myStruct.myMethod1()
在最后一行代码上设置个断点,然后把程序跑起来,等调试器断下来之后,在LLDB的命令上执行:
disassemble --line
就能看到类似下面的结果:
而反汇编下0x100001ae0就会发现,这就是我们定义的myMethod1:
所以,在编译器生成的代码里:callq 0x100001ae0这样的调用,就叫做direct dispatch,方法的地址直接编码到汇编指令里,这种方式最简单,编译器甚至可以对这种调用采用inline的方式进行优化。但它也最不灵活,除了面向过程的编程方式之外,更多时候,我们需要的不是这种方法派发方式。
1.2 Table dispatch(函数表派发)
在面向对象编程的世界中,对于多态的支持,大多是是通过一种叫做”虚函数表”的方式实现的,函数表中使用了数组
来存储声明的每个方法的指针,在Swift中称为witness Table。每个类都维护一张属于自己的函数表,里面记录着所有函数;子类会复制一张父类的表,以便完成继承操作,在子类重写方法时修改指针,指向覆盖的新函数,子类添加的新函数会被插入表的最后。每当调用函数时(也就是程序运行时),根据函数表的指针来确定具体调用哪个函数。
class ParentClass {
func method1() {
}
func method2() {
}
}
class ChildClass: ParentClass {
override func method2() {
}
func method3() {
}
}
这时,编译器会创建两张函数表,一个是ParentClass 的,一个是ChildClass的:
let obj = ChildClass()
obj.method2(