Swift底层探索(四)Swift函数调用过程的探索

3,361 阅读7分钟

我正在参加「掘金·启航计划」

OC方法的底层调用过程大家都很清楚了,但Swift并不存在Runtime,因此本文主要分析Swift中的类和结构体的方法存储在哪里,以及如何调用的

主要内容

  1. 静态派发
  2. 动态派发

1. 静态派发

值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针。这个函数指针在编译、链接完成后就已经确定了,存放在代码段。

结构体属于值类型,因此结构体内部并不存放方法。可以直接通过地址直接调用。

1.1 查看

1.1.1、调试

调试

说明:

  • 查看可以发现结构体中函数的调用,是直接通过地址来调用
  • 这种通过地址直接调用的方式就是静态派发
  • 那么这个函数地址是存储在哪里的呢

1.1.2、Mach-O查看

16620429104508.jpg

说明:

  • 这个地址是存储在Mach-0中的__text,也就是代码段中
  • 需要执行的汇编指令都在这里

1.1.3、符号查看

对于上面的分析,还有个疑问:直接地址调用后面是符号,这个符号哪里来的?

符号

企业微信截图_30a59201-59f1-4acb-8f04-0f12ed5f3ebd.png

说明:

  • 在静态调用中,会看到关于这个地址的符号
  • 地址我们已经知道是存储在了__text中
  • 那么这个符号存储在哪里呢,查看符号表

符号表:

16620429318695.jpg

说明:

  • 符号可以通过符号表来查找
  • 但是符号表中并不存储字符串
  • 具体的字符串会直接存储到字符串表中
  • 符号表存储的是相应字符串在字符串表中的地址
  • 然后根据符号表中的偏移值到字符串中查找对应的字符
  • 此时会进行命名重整,工程名+类名+函数名

字符串表:

16620429455570.jpg

说明:

  • 字符串表,存放了所有的变量名和函数名,以字符串形式存储
  • 因为在符号表中函数名偏移了两个字节,因此这里前两个字节存储的 就是该函数名

注意:

*如果在release下,是不会存储符号的,直接存储静态链接的地址

  • 一旦编译完成,其地址确定后,当前的符号表就会删除当前函数对应的符号
  • 在release环境下,符号表中存储的只是不能确定地址的符号
  • 对于不能确定地址的符号,是在运行时确定的,即函数第一次调用时(相当于懒加载)

1.2 函数符号命名规则

1.2.1 C函数

对于C函数来说,命名的重整规则就是在函数名之前加_。因此C中不允许函数重载,因为在底层的函数符号没有办法区分

16620429637897.jpg

1.2.2 OC函数

OC函数的符号命名规则是-[类名 函数名]。因此也是不可以重载的,因为如果有重载的函数,重载是参数和返回值的差异,而在底层符号无法区分

16620429763066.jpg

1.2.3 Swift函数

Swift的命名规则更加复杂,会带有参数和返回的差异,因此是可以确保函数符号的唯一性,也就是可以进行函数重载了

16620429952739.jpg

2. 动态派发

2.1 函数表(V_Table)的认识

在SIL文件中的格式:

//声明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

代码:

企业微信截图_c6fe4906-81bb-4e22-94fa-d84a2be977d1.png

SIL中V_Table:

企业微信截图_899433d0-8641-4384-99da-a59246098d7f.png

说明:

  • sil_vtable:关键字,表示vtable
  • WYTeacher表示某个类的函数表
  • 接下来是方法定义
  • init方法和deinit方法
  • 方法按顺序存储d奥函数表中

2.2 函数表的理解

函数表用来存储类中的方法,存储方式类似于数组,方法连续存放在函数表中。

查看方法地址:

16620427588724.jpg

说明:

  • 观察这几个方法的偏移地址,可以发现方法是连续存放的,偏移8个字节
  • 正好对应V-Table函数表中的排放顺序,即是按照定义顺序排放在函数表中

2.3 函数表源码探索

在源码中查看函数表的具体实现,通过initClassVTable来初始化类的函数表。

源码:

企业微信截图_22c19caa-f5f4-48e8-9c1f-a464e62938c9.png

说明:

  • initClassVTable就是用来创建一个类的V_Table表的方法
  • 可以看到其内部是通过for循环编码,然后offset+index偏移拿到Method地址
  • 之后将方法地址存入到偏移后的内存中,从这里可以印证函数是连续存放的

2.4 扩展中的函数如何调度

给一个类增加extension,子类继承自该类,那么子类会继承这个extension中的方法吗

代码:

extension WYTeacher {
    func teach5() {
        print("teach5")
    }
}

class WYStudent: WYTeacher {
    func teach6() {
        print("teach6")
    }
}

SIL文件

企业微信截图_bc085bab-82ba-4091-b9bc-1c7b81eb8338.png

说明:

  • 可以看到子类并没有继承扩展中的方法
  • 子类只继承了函数表中的函数
    • 这是因为子类有父类方法和子类方法,如果扩展中的方法插入到子类的函数表中,此时无法区分往哪里插
    • extension中的方法是直接调用的,且只属于类,子类是无法继承的

2.5 特殊修饰符的函数

2.5.1 final

final 修饰的方法是 直接调度的,可以通过SIL验证

代码:

/*
 3、final
 */
class WYTeacher {
    final func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

SIL查看:

企业微信截图_c1e1f1ec-5dbf-4978-9867-f59a37acf146.png

说明:

  • 明显看到在sil_vtable中没有存储teach1
  • 打断点也可以查看到teach是直接调度
  • 因此final修饰的函数没有存储在函数表中

2.5.2 @objc

使用@objc关键字是将swift中的方法暴露给OC,@objc修饰方法的调度方式是函数表调度。

代码:

/*
 4、@objc
 */
class WYTeacher{
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

SIL文件:

企业微信截图_450ba094-7f9d-41a8-b811-85dcbd13d301.png

说明:

  • 可以看到在函数表中存储有teach方法
  • 并且断点调试中也是有teach的

2.5.3 dynamic

dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling

代码:

企业微信截图_b07da98c-38b7-45d1-a731-34f426ed707a.png

SIL文件:

企业微信截图_c4965744-1b31-4e97-ad1c-9c2fcea890be.png

说明:

  • 其中teach函数的调度还是 函数表调度

@objc + dynamic的实现: @objc + dynamic可以实现消息发送

企业微信截图_d35c66f6-6a7f-455c-bb87-7c668d4592ca.png

说明:

  • 可以看到在底层使用objc_msgSend来发送消息调用
  • 也容易理解,这里其实是以OC的方法调用形式

方法交换实现:

企业微信截图_a335ca14-1591-4974-98c4-afdcc236c6b9.png

说明:

  • 可以看到teach和teach5已经发生了交换
  • 只要通过@_dynamicReplacement,将当前方法teach5和参数中teach进行转换

2.6 总结

注意:

  1. 继承方法和属性,不能写extension中。
  2. 而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写
  3. OC访问Swift,会生成OC和Swift的两种方法
    1. swift原有的函数
    2. @objc标记暴露给OC来使用的函数: 内部调用swift的
  4. @objc+@dynamic又可以动态,又可以暴露给OC,这样才可以使用消息转发

总结:

  1. 对于class中函数来说,类的方法调度是通过V-Taable,其本质就是一个连续的内存空间(数组结构)。
  2. extension中的方法是直接调用的,且只属于类,子类是无法继承的
  3. final修饰的函数调度方式是直接调度
  4. @objc修饰的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject
  5. dynamic修饰的函数的调度方式是函数表调度,使函数具有动态性
  6. @objc + dynamic 组合修饰的函数调度,是执行的是objc_msgSend流程,即 动态消息转发