Swift 派发机制

前言

对于编译型语言来看,有主要三种类型的函数派发方式,分别为:

  • Direct Dispatch:直接派发

  • Table Dispatch:函数表派发

  • Message Dispatch:消息派发

分析三种派发方式主要从性能动态性两方面讨论,这两个特性相对而言是矛盾的,性能要求高,则动态性差,反之亦然,其中直接派发又被称为静态派发,函数表派发与消息派发称为动态派发,大多数语言都会支持上面派发方式的一种到多种。如

  • C 使用直接派发;

  • Java 默认使用函数表派发,可以通过 final 修饰符修改成直接派发;

  • C++ 默认使用直接派发,但可以通过加上 virtual 修饰符来改成函数表派发;

  • OC 使用直接派发、消息派发方式;(普通方法采用消息派发的方式,load 方法使用直接派发的方式)

直接派发

直接派发是三种形式里面最快速的,在编译时就确定了方法的调用地址,汇编代码中,直接跳到方法的地址执行,生成的汇编指令最少。

优点:编译器可以对这种派发方式进行更多优化,比如函数内联等。
缺点:缺乏动态性,无法实现继承等;

函数表派发

函数表是编译型语言常见的派发方式,函数表使用数组来存储类中声明的每个函数的指针。对于这个表,大部分语言叫 Virtual table(虚函数表) 。根据 Swift 编译生成的 SIL 文件分析,Swift 中存在两种函数表,其中协议使用的是 witness_table (SIL 文件中名为 sil_witness_table),类使用的是 virtual_table(SIL 文件中名为 sil_vtable)。

每一个类都会维护一个函数表,里面记录着类所有的函数,如果父类函数被 override,表里面只会保存被 override 之后的函数。一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数;

一个函数被调用时会先去读取对象的函数表(读取第一次),再根据类的地址加上该的函数的偏移量得到函数地址(读取第二次),最后跳到那个地址上去(跳转一次)。整个过程是两次读取一次跳转,比直接派发慢一些。

图片

消息派发

消息派发是动态性最强的派发方式,也是性能最差的一种方式;方法调用包装成消息,发给运行时(相当于中间人),运行时会找到类对象,类对象会保存类的数据信息,或通过父类查找,直到命中执行,如果没找到方法,抛出异常,运行时提供了很多动态的方法用于改变消息派发的行为,相比函数表派发有很强的动态性,由于运行时支持的功能很多,方法查找的过程比较长,所以性能比较低;

OC 消息派发过程在这不展开说,后续有博文专门说这个。

Swift 中的函数派发

分析SIL文件,我们可以分析出Swift中派发方式的规律,关于SIL相关知识,可以参照该文 iOS编译简析 。

本文只给出关键命令 swiftc main.swift -emit-sil | xcrun swift-demangle > main.sil

派发方式与 SIL 文件中关键指令对应关系

  • sil_witness_table/sil_vtable:函数表派发

  • objc_method:消息机制派发

  • 不在上述范围内的属于直接派发;

Swift 语言支持三种派发方式。采用何种方式跟以下四种因素相关:

  • 声明的位置

  • 引用类型

  • 指定行为

  • 显式地优化

|

直接派发函数表派发消息派发
NSObject@nonobjc 或者 final 修饰的方法声明作用域中方法
Class不被 @objc 修饰的扩展方法及被 final 修饰的方法声明作用域中方法
Protocol扩展方法声明作用域中方法
Value Type所有方法
其他全局方法,staic 修饰的方法;使用 final 声明的类里面的所有方法;使用 private 声明的方法和属性会隐式 final 声明;

通过该表格你大概就可以理解一下 Swift 语言中的一些限制了:

  • extension 中定义的方法如果想 overrite,需要在方法上加上 @objc 修饰符;因为如果不加 @objc,走的是直接派发,无法重写方法。

Swift 派发优化

内联优化

Swift 编译时在直接派发方式的基础上还可以进行优化,如函数内联。

内联主要原理是:将一些函数的实现直接编译入调用函数的位置中去,减少函数指针的栈调用,提高运行效率。当开启编译优化 (Optimization Level) 时,编译器会在直接派发方式基础上根据函数实际情况进行内联优化。下列情况编译器默认不会进行内联优化:

  • 函数体过长(无形中增加了包体积,重复代码);

  • 函数包含动态派发;

  • 函数中包含递归调用;

Swift 中显式内联优化修饰符

  • @inline(never) 声明这个函数 never 永远不被编译成 inline 的形式,即使开启了编译器优化;

  • @inline(__always) 声明这个函数总是编译成 inline 的形式, 这个修饰符只对函数体过长这种不会被内联优化的情况生效,其他情况也不生效;

内联除了可以提高运行效率这个优点之外,还有另外一个好处,将部分关键函数进行内联优化,可以增大逆向难度。

尽量直接派发

Swift 会尽可能的优化派发方式,一些函数表派发方法会优化成直接派发。编译器可以通过 whole module optimization 检查继承关系,对某些没有标记 final 的类通过计算,如果能在编译期确定执行的方法,则使用直接派发。比如一个函数没有 override,Swift 就可能会使用直接派发的方式。


有一个技术的圈子与一群志同道合的朋友非常重要,来我的技术公众号及博客,这里只聊技术干货。

  • 微信公众号:CoderStar

  • 博客:CoderStar’s Blog

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值