我正在参加「掘金·启航计划」
OC方法的底层调用过程大家都很清楚了,但Swift并不存在Runtime,因此本文主要分析Swift中的类和结构体的方法存储在哪里,以及如何调用的
主要内容
- 静态派发
- 动态派发
1. 静态派发
值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针。这个函数指针在编译、链接完成后就已经确定了,存放在代码段。
结构体属于值类型,因此结构体内部并不存放方法。可以直接通过地址直接调用。
1.1 查看
1.1.1、调试
说明:
- 查看可以发现结构体中函数的调用,是直接通过地址来调用
- 这种通过地址直接调用的方式就是静态派发
- 那么这个函数地址是存储在哪里的呢
1.1.2、Mach-O查看
说明:
- 这个地址是存储在Mach-0中的__text,也就是代码段中
- 需要执行的汇编指令都在这里
1.1.3、符号查看
对于上面的分析,还有个疑问:直接地址调用后面是符号,这个符号哪里来的?
符号
说明:
- 在静态调用中,会看到关于这个地址的符号
- 地址我们已经知道是存储在了__text中
- 那么这个符号存储在哪里呢,查看符号表
符号表:
说明:
- 符号可以通过符号表来查找
- 但是符号表中并不存储字符串
- 具体的字符串会直接存储到字符串表中
- 符号表存储的是相应字符串在字符串表中的地址
- 然后根据符号表中的偏移值到字符串中查找对应的字符
- 此时会进行命名重整,工程名+类名+函数名
字符串表:
说明:
- 字符串表,存放了所有的变量名和函数名,以字符串形式存储
- 因为在符号表中函数名偏移了两个字节,因此这里前两个字节存储的 就是该函数名
注意:
*如果在release下,是不会存储符号的,直接存储静态链接的地址
- 一旦编译完成,其地址确定后,当前的符号表就会删除当前函数对应的符号
- 在release环境下,符号表中存储的只是不能确定地址的符号
- 对于不能确定地址的符号,是在运行时确定的,即函数第一次调用时(相当于懒加载)
1.2 函数符号命名规则
1.2.1 C函数
对于C函数来说,命名的重整规则就是在函数名之前加_。因此C中不允许函数重载,因为在底层的函数符号没有办法区分
1.2.2 OC函数
OC函数的符号命名规则是-[类名 函数名]。因此也是不可以重载的,因为如果有重载的函数,重载是参数和返回值的差异,而在底层符号无法区分
1.2.3 Swift函数
Swift的命名规则更加复杂,会带有参数和返回的差异,因此是可以确保函数符号的唯一性,也就是可以进行函数重载了
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
代码:
SIL中V_Table:
说明:
- sil_vtable:关键字,表示vtable
- WYTeacher表示某个类的函数表
- 接下来是方法定义
- init方法和deinit方法
- 方法按顺序存储d奥函数表中
2.2 函数表的理解
函数表用来存储类中的方法,存储方式类似于数组,方法连续存放在函数表中。
查看方法地址:
说明:
- 观察这几个方法的偏移地址,可以发现方法是连续存放的,偏移8个字节
- 正好对应V-Table函数表中的排放顺序,即是按照定义顺序排放在函数表中
2.3 函数表源码探索
在源码中查看函数表的具体实现,通过initClassVTable来初始化类的函数表。
源码:
说明:
- 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文件
说明:
- 可以看到子类并没有继承扩展中的方法
- 子类只继承了函数表中的函数
- 这是因为子类有父类方法和子类方法,如果扩展中的方法插入到子类的函数表中,此时无法区分往哪里插
- 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查看:
说明:
- 明显看到在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文件:
说明:
- 可以看到在函数表中存储有teach方法
- 并且断点调试中也是有teach的
2.5.3 dynamic
dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling
代码:
SIL文件:
说明:
- 其中teach函数的调度还是 函数表调度
@objc + dynamic的实现: @objc + dynamic可以实现消息发送
说明:
- 可以看到在底层使用objc_msgSend来发送消息调用
- 也容易理解,这里其实是以OC的方法调用形式
方法交换实现:
说明:
- 可以看到teach和teach5已经发生了交换
- 只要通过@_dynamicReplacement,将当前方法teach5和参数中teach进行转换
2.6 总结
注意:
- 继承方法和属性,不能写extension中。
- 而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写
- OC访问Swift,会生成OC和Swift的两种方法
- swift原有的函数
- @objc标记暴露给OC来使用的函数: 内部调用swift的
- @objc+@dynamic又可以动态,又可以暴露给OC,这样才可以使用消息转发
总结:
- 对于class中函数来说,类的方法调度是通过V-Taable,其本质就是一个连续的内存空间(数组结构)。
- extension中的方法是直接调用的,且只属于类,子类是无法继承的
- final修饰的函数调度方式是直接调度
- @objc修饰的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject
- dynamic修饰的函数的调度方式是函数表调度,使函数具有动态性
- @objc + dynamic 组合修饰的函数调度,是执行的是objc_msgSend流程,即 动态消息转发