Swift-类与结构体(2)
在这里, 我们从函数的角度来出发看类与结构体
一、函数相关的修饰符
1.mutating修饰符
前提:在Swift中class 和struct中都可以定义方法,但是在默认情况下值类型的属性是无法被自身的实例方法修改的
struct Student{ var x = 0.0 var y = 0.0 func move(){ self.x += deltaX self.y += deltaY } mutating func move(x deltaX:Double,y deltaY:Double){ self.x += deltaX self.y += deltaY } }
mutating修饰符可以允许值类型属性被修改,那么,我们从SIL中间代码的角度来看这两个的区别(第一行是第一个函数生成的,第二行是mutating函数生成的):
1 function_ref @$s4main7SHPointV3sumyyF : $@convention(method) (SHPoint) -> () 2 function_ref @$s4main7SHPointV6moveBy6deltaX0E1YySd_SdtF : $@convention(method) (Double, Double, @inout SHPoint) -> ()
当不使用mutating时,函数参数传递的是SHPoint ,而使用mutating时,函数参数传递的是@inout SHPoint。那么这两者又什么区别呢?我们就需要了解一下inout这个修饰符
2.inout修饰符
inout即输入输出参数,他可以使函数内部修改外部实参的值(当函数运行之后,age的值需要在函数结束后依然保持改变)。
var age = 10
func modifyage(_age: inout Int){
age += 1
}
modifyage(&age)
print(age)//11
inout修饰符实际上是引用传递。说明在使用mutating函数时,我们实际上传递的是实例的地址,所以此时结构体才可以被自身的实例方法修改自身的属性方法
3.final修饰符
final修饰符被标记时说明函数无法被重写,,使用静态派发(在编译之前,当前值类型的位置就已经被确定)的方式,不会在vTable(虚函数表)中出现,且对objc运行时不可见。final常用来优化类,如果类中的方法不会被重载,可以加final来使其变为静态调用。
4.dynamic修饰符
dynamic在修饰时为非objc类和值类型赋予动态性(在继承中可以被动态替换),派发方式采用函数表派发;另外dynamic不会改变函数的派发方式。
5.@objc修饰符
@objc修饰符使得swift函数暴露给objc运行时,但依然是函数表派发,如果将4+5结合到一起,也就是@objc+dynamic,就会形成消息派发机制,也就是OC中的objc_msgsend消息传递,这样就为swift和oc混编制造了机会。
二、方法调用
在OC中都是基于objc_msgsend函数来查找方法并进行调用的,而swift中又是如何调用的呢?我们通过lldb调试下面代码来看看
struct SHPoint{ func test1(){} func test2(){} func test3(){} } var p = SHPoint() p.test1() p.test2() p.test3()
通过打断点,我们进入到汇编语言中发现,在调用结构体的方法时,其实是直接拿到函数的地址进行调用的。
三、类的方法
我们新建一个 Swift 项目,需要注意的是,一定要用真机跑,因为我们的 iOS 程序都是要装到手机上的,而手机的架构目前基本都是 arm64 的架构。
定义一个 SHPerson
类型,调用方法,打个断点,来看一下 Swift 类的方法在汇编的调用情况(bl,blr都表示跳转到某指令,blr表示无返回值)。
class LGTeacher{ func teach(){}
func teach1(){}
func teach2(){} } class ViewController: UIViewController{ override func viewDidLoad() { let t = LGTeacher() t.teach()
t.teach1()
t.teach2() } }
在上面的汇编代码中我们发现,其中的三次blr即为三次调用teach函数的过程,从汇编代码中我们发现,其实teach的调用过程是通过类拿到实例对象,同时拿到metadata的地址后通过内存平移的方式从而拿到函数地址再进行调用的。那么我们这些连续的函数地址又放在哪里呢?此时我们就需要了解一下虚函数表了。
1.虚函数表(VTable)
在swift对象组成中有一个metadata,这个结构体中有个typeDescriptor属性,这个属性是对类的一个详细的描述,在对这个属性的查找过程中可以发现,其内部有一个addVtable函数,在这个函数的实现中有这样一段代码:
在这里,计算 offset
(结构体中的成员变量所有内存大小之和)之后,调用了 addInt32()
函数去计算添加方法到虚函数表的偏移量,最后再通过 for 循环,添加函数的指针。 总的来说:函数表添加函数的形式就是追加到数组的末尾。所以呢,函数表是按顺序连续存储类的方法的指针。
四、MachOView来分析类的方法存储
1.Mach-O文件
Mach-O(Mach Obejct)文件实际上是mac以及iOS上的可执行文件的格式,Mach-O文件的结构如下所示:
- 文件头,表明该文件是Mach-O格式,指定目标架构,还有其他的一些文件属性信息,文件头信息影响后续的文件结结构安排
- Load commands是一张包含很多内容的表,内容包括区域的位置,符号表,动态符号等
- Data区负责记录代码和数据记录。Mach-O文件是以segment这种结构来组织数据的,一个segment可分为多个section,每个section可认为是代码、常量或者是其他的一些数据结构,在装载内存中时,是根据segment来做内存映射的。
2.Mach-O文件如何打开
首先,我们需要现在xcode中生成当前项目的projects文件(https://blog.csdn.net/u012275628/article/details/121140428?spm=1001.2014.3001.5501)
其次,我们点击projects中的app-show in finder-点击app-显示包内容-找到一个黑框的执行文件拖入MachOview中即可显示
swift5_types 这里存放的是结构体、枚举、类的 Descriptor
,那么我们可以在 swift5_types 这里找到类的 Descriptor
的地址信息。 右侧展示地址信息。
前面的四个字节 90 FB FF FF
就是 类 的 Descriptor
信息(iOS 属于小端模式,所以 90 FB FF FF
要从右边往左读)
五、extension修饰符
我们先通过一个例子来看看带有extension修饰符的方法是怎么调用的?(汇编需要在真机的arm64架构上才能显示)
class Student{ func t(){} } extension Student{ func t2(){} } class ViewController: UIViewController{ override func viewDidLoad() { var s = Student() s.t() s.t2() } }
通过对t2打断点,我们可以发现:extension修饰的方法是直接通过地址调用的,而没有加入到vtable中。
这样做的目的是为了优化,如果把extension修饰的方法在加入到函数表中需要进一步的考虑方法的存储位置,索引等等,但是如果我们直接静态派发就可以避免这些操作啦。
因此,对于extension调度的方法都是静态派发的。