Swift派发机制

作者: 吕建雄 | 来源:发表于2021-08-16 12:12 被阅读0次

    Swift派发分:静态派发和动态派发

    静态派发:(又叫:直接调用)

    静态派发机制,同时支持值类型和引用类型;静态派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间, 例如函数内联等,. 静态派发也有人称为直接调用.

    然而, 对于编程来说直接调用也是最大的局限, 而且因为缺乏动态性所以没办法支持继承

    动态派发:

    动态派发机制仅支持引用类型(reference types) 比如:Class。简而言之:对于动态性或者动态派发,我们需要使用到继承特性,而这是值类型不支持的。动态派发是调用函数最动态的方式. 也是 Cocoa 的基石, 这样的机制催生了 KVO, UIAppearence 和 CoreData 等功能. 这种运作方式的关键在于开发者可以在运行时改变函数的行为. 不止可以通过 swizzling 来改变, 甚至可以用 isa-swizzling 修改对象的继承关系, 可以在面向对象的基础上实现自定义派发.

    函数表派发:

    函数表派发是编译型语言实现动态行为最常见的实现方式. 函数表使用了一个数组来存储类声明的每一个函数的指针. 大部分语言把这个称为 "virtual table"(虚函数表), Swift 里称为 "witness table". 每一个类都会维护一个函数表, 里面记录着类所有的函数, 如果父类函数被 override 的话, 表里面只会保存被 override 之后的函数. 一个子类新添加的函数, 都会被插入到这个数组的最后. 运行时会根据这一个表去决定实际要被调用的函数.

    查表是一种简单, 易实现, 而且性能可预知的方式. 然而, 这种派发方式比起静态派发还是慢一点. 从字节码角度来看, 多了两次读和一次跳转, 由此带来了性能的损耗. 另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化. (如果函数带有副作用的话)

    这种基于数组的实现, 缺陷在于函数表无法拓展. 子类会在虚数函数表的最后插入新的函数, 没有位置可以让 extension 安全地插入函数. 这篇提案很详细地描述了这么做的局限.

    4种派发机制:

    1、内联(inline)最快

    2、静态派发(Static Dispatch)

    3、函数表派发(Virtual Dispatch)

    4、动态派发(Dynamic Dispatch)(最慢)

    由编译器决定应该使用哪种派发技术。当然,优先选择内联,然后按需选择

    静态派发VS动态派发Swift VS OC

    OC默认支持动态派发,这种派发形式以多态的形式为开发人员提供了灵活性。

    比如:子类可以重写父类的方法

    动态派发以一定的运行时开销为代价,提供了语言的灵活性。

    在动态派发机制下,对于每个方法的调用,编译器必须在方法列表中查找执行方法的实现。

    编译器需要判断调用方,是选择父类的实现还是子类的实现,而且由于所有对象的内存都是在运行时分配的,因此编译器只能在运行时执行检查。

    而静态调用则没有这个问题。在编译期的时候,编译器就知道要为某个方法调用某种实现。因此编译器可以执行某些优化,甚至在可能的情况下,可以将某些代码转换成inline函数,从而使整体执行速度更快

    如何在Swift中使用动态派发和静态派发?

    1、要实现动态派发,可以使用继承,重写父类的方法。另外我们可以使用dynamic关键字,并且需要在方法或类前面加上关键字@objc,以便方法公开给OC runtime使用

    2、要实现静态派发,我们可以使用final和static关键字,保证不会被覆写

    静态派发:

    和动态派发相比,非常快。编译器可以在编译器定位到函数的位置。因此函数被调用时,编译器能通过函数的内存地址,直接找到它的函数实现。极大的提高了性能,可以达到类型inline的编译期优化

    动态派发:

    在这种类型的派发中,在运行时而不是编译时选择实现方法,会增加运行时的性能开销。

    优势是:具有灵活性(大多数的OOP语言都支持动态派发,因为它允许多态)

    动态派发有两种形式:

    1、函数表派发(Table Dispatch)

    这种调用方式利用一个表,该表是一组函数指针,称为witness table,以查找特定方法的实现

    witness table如何工作?

    每个子类都有它自己的表结构

    对于类中每个重写的方法,都有不同的函数指针

    当子类添加新方法时,这些方法指针会添加在表数组的末尾

    最后,编译器在运行时使用此表来查找调用函数的实现

    由于编译器必须从表中读取方法实现的内存地址,然后跳转到该地址,一次它需两条附加指令,因此它比静态派发慢,但仍比消息派发快

    2、消息派发(Message Dispatch)

    这种动态派发方式是最动态的。事实上它表现优异,目前Cocoa框架在KVO,CoreData等很多地方在使用它

    此外,它还可以使用method swizzling,可以在运行时更改函数的实现。

    Swift本身不支持消息派发,而是利用OC的runtime特性,间接实现这种动态性。

    要使用动态性需要使用dynamic关键字。Swift4.0之前,需要一起使用dynamic和@objc。Swift4.0之后,只需表明@objc让方法支持oc的调用,以支持消息派发

    具体通过如下代码来深入体会:

    /*

    值类型:

    由于struct和enum都是值类型,不支持继承,编译器将它们置为静态派发下,因为它们永远不可能被子类化

    */

    struct LTPerson {

        //静态派发

        func chinesePerson() -> Bool {

            return true

        }

    }

    extension LTPerson {

        ////静态派发

        func canDispatch() -> Bool {

            return true

        }

    }

    /*

    协议

    这里重点是在extension(扩展)里面定义的函数,使用静态派发(static dispatch)

    */

    protocol LTAnimal{

        //函数表派发

        func isAnimal() -> Bool

    }

    extension LTAnimal{

        //静态派发

        func canDispatch() -> Bool{

            return true

        }

    }

    /*

    普通方法声明遵循协议的规则

    当将方法公开给OC runtime是使用@objc,使用动态派发

    当一个类表标记为final是,该类不能被子类化,使用静态派发

    */

    class LTCat: LTAnimal{

        //函数表派发

        func isAnimal() -> Bool {

            return true

        }

        //动态派发

        @objc dynamic func hoursSleep() -> Int{

            return12

        }

    }

    extension LTCat{

        //静态派发

        func canSwim() -> Bool{

            return false

        }

        //动态派发

        @objc func goWild(){

        }

    }

    final class LTEmployee{

        //静态派发

        func canSpeak()->Bool{

            return true

        }

    }

    通过上面的代码可以知道是使用的哪种派发方式;具体可以通过sil文件来进行验证

    生成SIL文件命令:swiftc -emit-sil main.swift >> main.sil

    如果使用table派发,则会出现在value(或者witness_table)中

    vtable

    如果使用动态转发,则能找到objc_method标记,指示使用OC运行时调用了该函数

    objc_method

    如果没有出现以上两种情况的标记,则是静态派发

    相关文章

      网友评论

        本文标题:Swift派发机制

        本文链接:https://www.haomeiwen.com/subject/insmbltx.html