Swift-类与结构体(2)

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修饰符被标记时说明函数无法被重写(final修饰的类不允许被继承),使用静态派发(在编译之前,当前值类型的位置就已经被确定)的方式,不会在vTable(虚函数表)中出现,且对objc运行时不可见。final常用来优化类,如果类中的方法不会被重载,可以加final来使其变为静态调用。

4.dynamic修饰符

dynamic在修饰时为非objc类和值类型赋予动态性(在继承中可以被动态替换),派发方式采用函数表派发;另外dynamic不会改变函数的派发方式。

5.@objc修饰符

@objc修饰符使得swift函数暴露给objc运行时,但依然是函数表派发,如果将4+5结合到一起,也就是@objc+dynamic,就会形成消息派发机制,也就是OC中的objc_msgsend消息传递,这样就为swift和oc混编制造了机会。

6.private/filepeivate

当使用private/filepeivate修饰时,表明该对象仅在当前文件内可见

  • privtae:定义的声明中访问(对类的继承没有影响)
  • fileprivate:只在定义的源文件中中访问

二、方法调用

在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调度的方法都是静态派发的。 

六、函数内联

函数内联是一种编译器优化的方式,它通过使用方法的内容来替换直接调用该方法,从而优化性能。

  • @inline(_always)是始终内联函数的标识
  • @inline(_never)是始终不内联函数的标识,这种情况一般用于函数很长但是想避免增加代码段大小。

 

 

posted on 2022-01-13 10:50  suanningmeng98  阅读(71)  评论(0编辑  收藏  举报