Swift函数调用方式浅析

发布时间 2023-07-18 17:59:50作者: 滴水微澜
函数的调用机制
 
函数的调用机制是在函数调用时通过那种路径走到最终调用函数地址的机制。
在编程语言中,函数的调用机制有三种
1.静态调用:编译期就确定了函数内存地址,执行效率最高,还可以使用编译器优化如:inline函数内联提升执行效率。缺点:因为函数调用的内存地址在编译期已经确定,则无法支持继承等动态修改调用的方式。
2.函数表调用:每个类都有一份自己的v-table虚函数表,里面是以函数名为key, 函数地址为value; 如果子类override了父类的方法,那么这个方法名key对应的value就是那个子类重写的新的函数地址。
3.消息发送调用:所有的函数调用最后都会转换成一系列参数,通过消息发送的方式进行调用。这种方式最灵活,可以override重写父类方法,可以swizzle交互类中方法的实现,可以swizzle_isa修改类的父类即:修改继承链。
 
常见语言的调用机制
C++:默认使用静态调用机制,可以通过virtural修饰成使用函数表机制调用。
Java:默认使用函数表调用机制,可以通过final修饰成直接调用机制。
Object-C:只能使用消息发送的方式进行方法调用,可以使用C代码写直接调用机制的代码。
Swift:可以根据不同的修饰符,根据情况使用上面三种方式的任一方式。

Swift中的函数调用方式
Swift的调用方法非常灵活,它三种类型都支持。
首先在大的分类上分2种:Static Dispatch 和 Dynamic Dispatch。
Static Dispatch是静态调用,调用方法的函数地址是在编译时确定的。
Dynamic Dispatch是动态调用,调用的函数地址要在运行时才能确定。
Dynamic Dispatch动态调用又可以分为3个子类:V-Table Dispatch, witness table dispatch, objc_msgSend。

Swift的类型分2种,值类型和引用类型。值类型包含结构体和枚举,对于值类型中的方法调用基本都是静态调用,执行效率非常高。
引用类型就是对象,Swift中的类型分为是否继承自NSObject, 原因是如果继承NSObject,那么对象的结构体中(类的底层实现也是一个结构体)就有了OC运行时中那一套的所有机制,isa指针,方法列表,属性列表,协议列表等。从而函数调用就支持消息发送方式了。这也是Swift类中方法走消息发送的前提条件。

值类型-Struct
1.因为结构体不能继承,所以它的struct下定义的方法的调用都是静态的。
2.它的extense下扩展的方法不能被override,走的也是静态的。
3.调用遵守协议的方法实现就是自己定义的方法一样。

引用类型-纯Swift类
1.在Swift类中定义的方法,影响它调用方式的只有final关键字,正常定义的方法Swift通过一种Virtual Table的机制在运行时寻找方法的内存地址并调用。被final修饰的方法不能被override,走静态调用。
2.它的extense下扩展的方法不能被override,走静态调用。
3.调用遵守协议的方法实现就是自己定义的方法一样,走V-Table虚表调用。

引用类型-Swift类继承自NSObject
这种方式创建的Swift类,调用方式受关键字影响比较大。
1.被final修饰的,走静态调用,因为不能被override。
2.类中定义的普通方法和被@objc修饰的方法,都是走的V-Table方式调用。NSObject的子类+@objc修饰符的方法,只能表示可以让OC类进行调用,但真正的执行机制还是走的V-Table虚表调用。
3.@objc+dynamic修饰的方法走OC的runtime消息发送。
4.Extense下的方法默认走静态调用,因为不能被override。如果此时被@objc和dynamic修饰,就无法走静态,走的OC的runtime消息发送。
5.调用遵守协议的方法实现就是自己定义的方法一样。
上面是在没有做编译器优化的情况下,如果做了编译器优化,则编译器会尽可能走静态调用的方式,提高运行效率。
Protocal
1.当变量以当前对象的方式调用时,走的是当前对象定义方法的方式。
2.当变量以Protocal协议对象的方式调用时,走的是Witness Table
3.协议中被@objc修饰的方法,走的是runtime的消息发送

 

判断函数是以哪种方式调用的方法
静态调用
因为静态调用时,call函数的地址是固定的,根据machO文件加载到内存的内存分布

 

所以,静态调用时,函数地址是一个固定的,比栈变量内存地址小的内存地址。

消息发送
消息发送时,函数调用通常都会走到同一段函数内存地址中,因为所有的函数调用都是使用同一个消息发送方法进行的。
函数表调用
函数调用的实际内存地址通常需要根据偏移量动态计算而来, 不像静态调用和消息发送在汇编代码里有明显的特征。
可以通过汇编调试进行佐证上面的函数调用方式
查看寄存器中的值
register read/格式
register read/x

register write 寄存器名称 数值
register write rax 0
查看具体内存地址中的值
x/数值-格式-字节大小 内存地址
x/3xw 0x000010

memory write 内存地址 数值
memory write 0x000010 10
Swift和OC在模拟器调试时,采用的汇编类型是AT&T
常用寄存器说明:
rax寄存器常用于函数传参和返回值
rbp,rsp寄存器常用于栈数据的读取
rip常用于指令寄存器

常用指令说明:
movq $30 %rax  //将30数据放置在rax寄存器中
leaq -0x86(%rbp) %rax //将%rbp-0x86中的内存地址放置在rax寄存器中
call -0x86(%rbp) //函数调用,下面通常有ret命令对应
jump -0x86(%rbp)  //if跳转
常见表示说明:
0x4bdc(%rip),一般是全局变量,全局区(数据段)
-0x78(%rbp), 一般是局部变量,栈空间
0x10(%rax), 一般是堆空间

 

 

参考文章:
https://zhuanlan.zhihu.com/p/35696161
https://www.cnblogs.com/zhou--fei/p/17245908.html