Swift06 - ⽅法调度

706 阅读5分钟

Swift 进阶之路 文章汇总

前言:

上篇文章分析了Swift中的指针,以及指针Swift中的基本使用,这篇文章主要分析方法Swift中如何调用的,在swift中方法调度分为两种:

  • 静态调⽤(直接调用)

  • 查找调用(函数都按照顺序存储在vtable中,按照地址偏移方式进行调用)

静态调⽤

对于值类型对象的函数的调用方式是静态(直接)派发(调用),也可以说是直接地址调用(指针调用),也就是说,在编译,链接完成后当前函数的地址在Mach-O代码段就已经有了确定的位置。

Swift中典型的值类型就是结构体,那么我们就通过如下的分析,证明结构体中函数的调用是静态调用。

案例,在Xcode中创建:

struct LGTeacher {
    func teach() {}
}
var t = LGTeacher()
t.teach()
print("end")

通过汇编看函数的调用

在汇编的函数调用中,可以看到callq 直接调用的是0x100002c10地址,所以由此可看出Swift结构体的方法调用时直接地址派发

Mach-O验证

Mach-O中查看方法是否在__text段:

由上图可看出,方法的存储地址就在__text段中,所以可验证函数指针编译、链接完成后就已经确定了,存放在代码段

符号表Symbol Table验证

  • 查看Functions Starts中所有的方法的重命名,命名规则:工程名+类名+函数名

  • 查看符号表的地址偏移量

函数地址:0000000100002C10,和汇编中的地址一致

  • 查看Sting Table表

  • 用终端查看重命名符合表中s12NewSwiftDemo9LGTeacherV5teachyyF符号

  • 还原符号

动态派发

既然有静态派发,那么当然就会有动态派发,在Swift中的引用类型数据代表中的函数调用就是动态派发

知识补充

  • 汇编指令补充
  1. blr:带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址
  2. mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与起存起或者 寄存器与常量之间 传值,不能用于内存地址,mov x1,x0 将寄存器x0的值复制到寄存器x1中)
  3. ldr:将内存中的值读取到寄存器中(ldr x0,[x1,x2] 将寄存器x1和寄存器x2 相加作为地址,取该内存地址的值翻入寄存器x0中)
  4. str:将寄存器中的值写入到内存中(str x0, [x0, x8] 将寄存器x0的值保存到内存[x0 + x8]处)
  5. bl:跳转到某地址
  • vtable 简介

Swift中的vtableSIL文件中的格式:

//声明sil vtable关键字
decl ::= sil-vtable
//sil vtable中包含 关键字、标识(类名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法 包含了声明以及函数名称
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na
me

以LGTacher为例,其SIL中的v-table如下所示:

class LGTeacher{
    func teach(){}
    func teach2(){}
    func teach3(){}
    func teach4(){}
    @objc deinit{}
    init(){}
}

函数表 可以理解为 数组,声明在 class内部的方法在不加任何关键字修饰的过程中,是连续存放在我们当前的地址空间中的,汇编断点调试:

由上图可了解到函数表的首地址是一片连续的内存地址

源码分析

上面由汇编的方式探究了vtable的内存存储方式,说明在vtable中肯定有其创建的过程,打开swift源码,全局搜索:

其内部是通过for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,从这里可以印证函数是连续存放的,所以对于class中函数来说,类的方法调度是通过V-Taable,其本质就是一个连续的内存空间(数组结构)

extension 中的方法

创建LGTeacher的extension

class LGTeacher{
    func teach(){}
    func teach1(){}
    func teach2(){}
    func teach3(){}
    func teach4(){}
}
extension LGTeacher {
    func teach5(){}
}
var t = LGTeacher()
t.teach5()

查看SIL文件中的VTable方法,看是否在编译过程中存入到函数表中

由上图可看出,extension的方法在编译时并未存储到vtable表中,那extension中的方法是如何调用的呢?查看汇编代码:

 由上图可看出,extension直接callq的是0x100002b80函数地址,属于静态调用.

final、@objc、dynamic修饰函数

创建LGTeacherfinal,@objc,dynamic

class LGTeacher{
  final func teach(){}
    func teach1(){}
  @objc  func teach2(){}
    func teach3(){}
  dynamic  func teach4(){}
}
extension LGTeacher {
    func teach5(){}
}
var t = LGTeacher()
t.teach()
t.teach2()
t.teach4()
t.teach5()

  • final

查看SIL文件

可看出final修饰的函数并不在vtable中,查看汇编调用:

final关键字修饰的函数直接callq的是0x100002b80函数地址,属于静态调用.

  • @bjc

通过sil文件我们可以发现,使用@objc修饰的方法依旧存在VTable中,特别提醒:

对于使用@objc修饰的方法,实际上是生成了两个方法,其中一个就是我们Swift中原有的方法,另一个就是如上面代码所示的@objc方法,并在其内部调用了Swift原有的方法。所以使用@objc修饰的方法本质是,通过sil代码中的@objc方法调用,Swift中的方法来实现的。

  • dynamic

sil文件我们可以看到,使用dynamic修饰的方法是存放在VTable中的,目前没看到和swif中的方法有什么区别,官网中的定义为具有动态性.和@_dynamicReplacement关键字一起使用可以进行方法交换

  • @objc + dynamic

汇编断点调试:

可以看出走的是objc_msgSend流程,即 动态消息转发

总结

  • Swift中的方法调用分为静态调度动态调度两种

  • 值类型(例如struct)中的方法就是静态调度

  • 引用类型中的方法就是动态调度,其中函数的调度是通过V-Table函数表来进行调度的,即动态调度

  • extensionfinal中的函数调度方式是静态调度

  • @objc修饰的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject

  • dynamic修饰的函数的调度方式是函数表调度,使函数具有动态性

  • 使用dynamic@objc同时修饰的方法的调度方式是objc_msgSend