10、理解Swift中方法的派发机制

您所在的位置:网站首页 消息机制是什么 10、理解Swift中方法的派发机制

10、理解Swift中方法的派发机制

2024-07-15 20:14| 来源: 网络整理| 查看: 265

10、理解Swift中方法的派发机制 派发机制静态派发动态派发 1、Swift中有哪些派发方法1.1 Direct Dispatch(直接派发)1.2 Table dispatch(函数表派发)1.3 Message Dispatch(消息机制派发) 2、Swift是如何使用Objective-C消息派发机制3、Swift的方法派发规则3.1 值类型永远使用direct dispatch3.2 在protocol和class的定义中声明的方法使用table dispatch3.3 在protocol和class的extension中定义的方法使用direct dispatch3.4 NSObject以及其派生类使用的派发规则 4、修改方法派发规则的modifiers4.1 dynamic和@objc4.2 final和@nonobjc

派发机制

函数/方法把代码内聚到一处并对外暴露函数名,这提高了代码的复用性,也对外隐藏了具体的实现过程。根据函数名找到具体的函数实现,这就是函数派发的过程。函数派发的机制分两种:静态派发和动态派发。

静态派发

静态派发机制下,“方法的实现在编译期就已确定”,即编译器在编译期就已经能确定函数具体实现的位置在哪。调用函数时,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()

当一个函数被调用时, 会经历下面的几个过程: (1)读取对象里面的虚指针获取函数表地址0xB00 (2)读取对象 0xB00 的函数表. (3)读取函数指针的索引. 在这里, method2 的索引是1(偏移量), 也就是 0xB00 + 1. (4)跳到 0x222 (函数指针指向 0x222)

查表的过程是首先读取对象里面的虚函数表的指针获取虚函数表的地址,然后根据偏移量跳转到对应的函数指针。 这种基于数组的实现, 缺陷在于函数表无法拓展. 子类会在虚数函数表的最后插入新的函数, 没有位置可以让 extension 安全地插入函数。

1.3 Message Dispatch(消息机制派发)

消息机制是调用函数最动态的方式,也是Cocoa的基石,这样的运作方式的关键在于开发者可以在运行时改变函数的行为。 Objective-C的动态特性主要来自于查表。Objective-C的类有一张表,key是selector名称的字符串,value是方法的地址。

Objective-C派发机制中的核心: 把方法的调用转化为消息

objc_msgSend(id self, SEL op, ...)

参数1:调用者; 参数2:调用的方法; 省略号:方法的N个参数。

派发流程:

1、方法的调用者会通过isa指针找到其所属的类。2、runtime 会先去cache中查找对应的方法;3、若cache中没有找到则去methodLists中查找。4、找到后通过函数指针跳转到对应的实现中执行,并将方法加入到cache中以便下次查找;5、如果methodLists中也没找到,则继续顺着继承关系到父类中查找;6、如果直到根类 NSObject 都还没找到则会尝试动态方法决议或消息转发机制;7、如果没有实现这两种机制,则方法的调用会因找不到对应的实现而报运行时错误。

这样的方式效率明显低于依靠offset的C++ virtual table,不过动态性大大提高了,比如可以对nil对象send message,也可以调用不存在的selector,或者判断selector是否存在,还有只要selector存在,不需要显式的interface或是base class就能实现多态。

class ParentClass { dynamic func method1() {} dynamic func method2() {} } class ChildClass: ParentClass { override func method2() {} dynamic func method3() {} } let obj = ChildClass() obj.method2()

请添加图片描述

2、Swift是如何使用Objective-C消息派发机制

首先,我们把Base.method1改成这样:

class Base { dynamic func method1() {} }

重新执行后,当程序断在第一个断点之后,执行di -f,就可以找到类似下面的结果: 请添加图片描述 可以看到,编译器已经把b.method1变成了调用objc_msgSend,其中它的第一个参数%rdi是对象自身,也就是b,第二个参数$rsi是一个内存地址,这个地址保存着要调用方法的字符串,这也从某个侧面说明了OC中的SEL就是一个字符串。

最终,objc_msgSend会遍历对象链表以找到需要调用的方法。在Swift里,这是最灵活的一种调用方式。基于对象的isa指针,我们可以在运行时动态交换对象和方法。

3、Swift的方法派发规则 3.1 值类型永远使用direct dispatch

对于一个值类型来说,无论方法定义在类型自身还是extension里,调用方法永远都使用direct dispatch。

3.2 在protocol和class的定义中声明的方法使用table dispatch

这种方式和传统面向对象编程语言中的虚函数是最像的。通过witness table可以实现字面上一个基类类型的对象,实际上调用派生类方法的效果,也就是我们经常说到的多态。

class Base { func method1() { print("Base.method1") } } class SubClass:Base { override func method1() { print("SubClass.method1") } } //多态 let b:Base = SubClass() b.method1() // SubClass.method1 3.3 在protocol和class的extension中定义的方法使用direct dispatch

定义在extension中的方法是不能重写的。而这就是根本原因,编译器对这种方法会采用direct dispatch,它不支持运行时获取方法地址。 但是,一个让人容易困惑的情况是,如果自定义类型和protocol都通过extension实现了同样的方法,由于extension执行的direct dispatch机制,我们基于多态思维导致的直觉是错误的。例如:

protocol MyProtocol { } extension MyProtocol { func method3() { print("MyProtocol.method3") } } class Base: MyProtocol { func method1() {} func method3() { print("Base.method3") } }

注意在上面的例子里,method3是定义在extension MyProtocol中的,在MyProtocol的定义中,并没有声明这个方法。然后,你认为下面的代码会返回什么结果呢?

let b = Base() let p: MyProtocol = b b.method3() // Base.method3 p.method3() // MyProtocol.method3

由于p引用的实际上是个Base对象,我们会认为p.method3()会像多态的方式一样打印"Base.method3"。但实际上并不是,由于extension中的方法都采用了direct dispatch,p.method3()选择的方法直接是p变量的字面类型,也就是MyProtocol。

理解了这个例子之后,我们再来看一个在protocol定义中声明的方法的例子:

protocol MyProtocol { func method4() } extension MyProtocol { func method4() { print("MyProtocol.method4") } }

这次,我们在MyProtocol定义中声明了一个方法method4,并且用extension给它添加了一个默认实现。现在,我们在Subclass中重写method4方法:

class Base: MyProtocol { } class Subclass: Base { func method4() { print("Subclass.method4") } }

当我们执行下面的代码时:

let s = SubClass() s.method4() // SubClass.method4

你期望会得到什么结果呢?答案是:“SubClass.method4”,如果这是你第一次见到这个场景,这多少会让你感到意外。或者说,这算是当前Swift语言的一个Bug吧。如果Base从MyProtocol获得了默认的method4()方法,为什么在Subclass中重写method4时不使用override关键字可以通过编译呢?

唯一的解释,就是由于method4采用了table dispath,因此,它是通过SubClass的witness table完成调用派发的。但此时,method4并不属于Base和SubClass的继承关系。method4方法,只是SubClass和Base各自的一部分。

为了避免这个问题,一个解决方案就是:如果你的类型可能会被其他类型继承,你应该实现它遵从protocol的所有方法,哪怕其中一些已经有了默认实现。

于是,我们可以把之前的例子改成这样:

class Base: MyProtocol { func method4() { print("Base.method4") } } class Subclass: Base { override func method4() { print("Subclass.method4") } }

只要在Base中实现了method4,在派生类中重写时,就必须使用override了。这样所有在继承体系中实现了的method方法都会加入对应类对象witness table。我们之前的p.method4()就会打印"Subclass.method4"了。

3.4 NSObject以及其派生类使用的派发规则

在Swift中使用NSObject以及它的派生类时,Swift居然不会一股脑的统一使用message dispatch机制。对于在类定义内部的方法,调用仍旧会采用table dispatch;而对于定义在extension中的方法,才会采用message dispatch。

Swift原生的class在extension中是direct dispatch,而NSObject的派生类在extension中是message dispatch。也就是说,NSObject以及它的派生类定义在extension中的方法,是可以被重写的,但原生类在extension中的方法则不行。

并且,由于NSObject的派生类在Swift中采用了不同的派发机制,因此,我们在派生类的extension中重写基类的方法,是不生效的。来看下面这个例子:

class Base: NSObject { func method5() { print("Base.method5") } } class Subclass: Base { } extension Subclass { override func method5() { print("Subclass.method5") } }

由于Base从NSObject派生,我们就可以在Subclass的extension中重写基类的方法了。但是,你期望下面的代码返回什么结果呢?

let base: Base = Subclass() base.method5() // Base.method5

结果会是"Base.method5",这多少也会让人感到意外。base明明是一个Subclass对象,为什么method5却调用了基类的版本呢?

这是因为method5是定义在Base本体内的,此时,调用它执行的是table dispatch。但是,在Subclass的extension中重写的method5使用却是message dispatch,这个方法并不会被写在Subclass对象的witness table里。

因此,编译器只能在Subclass对象的witness table中找到method5的实现。于是,我们就看到基类的版本了。

为了解决这个问题,我们在Base.method5前面,加上dynamic关键字就好了:

class Base: NSObject { dynamic func method5() { print("Base.method5") } }

它可以强制让方法使用message dispatch进行派发。

4、修改方法派发规则的modifiers

除了刚才我们提到的基于方法所在的位置以及类型定义的派发规则之外,Swift还提供了一些modifiers,帮助我们在必要的时候,修改一个方法的派发规则。

4.1 dynamic和@objc

dynamic用于把方法的派发方式修改为message dispatch,同时可以被Objective-C运行时识别。当这个方法变成message dispatch之后,无论它定义在类型的定义中,还是在类型的extension中,就都是可以重写的了

@objc的作用仅仅是让一个Swift方法可以被Objective-C运行时识别和访问,但并不会改变这个方法的派发方式。

例如,在之前演示message dispatch的时候,我们定义的method1:

class Base { dynamic func method1() {} }

本来,调用method1应该是table dispatch的,使用dynamic修饰之后,就会变成message dispatch。当你的方法需要被Objective-C运行时识别时,就可以用它来修饰这个方法。当然,为了让你的方法使用message dispatch,你必须import Foundation,它包含了Objective-C运行时的核心组件。 另外,这样做还有一个副作用,当这个方法变成message dispatch之后,无论它定义在类型的定义中,还是在类型的extension中,就都是可以重写的了。

和dynamic类似的一个是@objc。但@objc的作用仅仅是让一个Swift方法可以被Objective-C运行时识别和访问,但并不会改变这个方法的派发方式。例如,当我们让一个Swift类从NSObject派生时,编译器就会默认给所有的方法加上@objc属性。

另外,当我们要在Swift中使用#selector选择一个方法的时候,编译器也会提示我们要使用@objc修饰这个方法。

4.2 final和@nonobjc

final用于把方法的派发方式修改成direct dispatch。而@nonobjc则仅仅让一个方法对Objective-C运行时不可见,但不会修改方法的派发方式。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3