Swift —— 类与结构体的方法

1. 异变方法

Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况 下,值类型属性不能被自身的实例方法修改。
在这里插入图片描述
如果想要自身的实例方法可以修改struct的属性,那么就需要在方法前面加上mutating的关键字。
在这里插入图片描述
那么mutating做了些什么呢?在结构体里添加一个没有mutating的方法和一个有mutating的方法,这样之后运行生成SIL文件方便进行观察。

struct Point {
    var x = 0.0, y = 0.0
    
    func test() {
        let tmp =  self.x
    }
    
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        //self
        x += deltaX
        y += deltaY
    }
}

看到SIL里面声明是没有变化的,那么接下来去看函数的实现。
在这里插入图片描述
看到test方法,这里需要一个参数Point,其实这个也就是self。在Swift中,不论是类还是结构体,都有一个默认参数,就是self。而在oc中,则有两个默认参数,一个是self,一个是sel。
在这里插入图片描述
看到moveBy方法这里,看到这里有三个参数,前面两个就是方法的参数,最后面就是默认参数self,而这里的默认参数和test方法的不同,Point前面加了@inout,并且这里可以看到在使用point的时候是用$*Point,而在test方法中使用的是 $Point。并且这里一个使用的是let,一个用的是var。也就相当于一个是 let self = Point。 一个是 var self = &Point。
在这里插入图片描述
在官方文档中解释到:An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址),也就是说,moveBy方法这里接受的是地址,而test方法接受的是实例。

所以我们得到:
异变方法的本质:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切,可以理解为外界将self的地址传入函数内部,所以内部修改self可以改变外部实例。
inout参数(输入输出参数):如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后 依然生效,那么就需要将形式参数定义为 输入输出形式参数 。在形式参数定义开始的时候在前边 添加一个 inout关键字可以定义一个输入输出形式参数

例如:


var age = 10

func changeAge(_ age: inout Int) {
    age += 1
}
changeAge(&age)
print(age)

在这里插入图片描述

2. 方法调度

在oc中使用的是objc_msgsend来进行方法的调度,接下来看一下swift的方法调度。

创建一个LGTeacher类,然后在viewDidLoad创建实例对象并且调用teach方法。

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        
        let t = LGTeacher()
        t.teach()
    }
}


class LGTeacher{
    func teach() {
        print("teach")
    }
    
}

2.1 汇编

在 t.teach()打下断点然后打开汇编模式后运行 。上篇文章可以知道生成实例调用__allocating_init(),那么teach在生成实例后面,而blr是跳转指令,那么blr 那一行就应该是调用teach方法。

在这里插入图片描述
进去之后发现果然到了teach方法里面。
在这里插入图片描述
为了探究Swift方法是怎么调度的,在LGTeacher类里面添加两个方法。

class LGTeacher{
    func teach() {
        print("teach")
    }
    func teach1() {
        print("teach")
    }
    func teach2() {
        print("teach")
    }
    
}

同时在viewDidLoad也调用这两个方法。

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        
        let t = LGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

分别打下断点后运行,看到了三个blr,也就是调用函数的地方。
看到这里调用的是x8,那么x8是怎么来的呢?往上看到:

  • mov x20,x0: 把寄存器x0的值复制到寄存器x20,这里的x0就是上面LGTeacher.__allocating_init()返回的实例对象

  • str x20, [sp, #0x18] : 把寄存器x20的值保存到栈内存 [sp + #0x18]

  • str x20, [sp, #0x20] : 把寄存器x20的值保存到栈内存 [sp + #0x20]

  • ldr x8, [x20] : 取寄存器x20地址的值放入到寄存器x8中,根据之前实例对象结构可以得知是metadata
    在这里插入图片描述

  • ldr x8, [x8, #0x50]: 取寄存器x8和0x50相加的值作为地址,取该内存地址放入到寄存器x8中,根据也就是metadata + 0x50 得到teach的方法的地址,然后放入到寄存器x8中。然后往下就是执行函数了。

同时,我们注意到,三个x8的地址相加的地址是分别是0x50,0x58,0x60,也就是相差8个字节,也就是函数指针的大小,在内存上是连续的内存空间。这种调用方法是基于函数表的调度。

在这里插入图片描述

2.2 SIL

将程序编译成SIL文件后打开,那么就看到这里有一个vtable,里面包含着teach,teach1,teach2方法,这个vtable其实就是每个类自己的函数表。
在这里插入图片描述

2.3 源码

再来看源码。这里需要关注一个Description的属性。
在这里插入图片描述
在看到TargetClassDescriptor,是一个复杂的类。
在这里插入图片描述
可以得出其结构大概为:

struct TargetClassDescriptor{
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32
    var metadataPositiveSizeInWords: UInt32
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32
    var Offset: UInt32
    var size: UInt32
    //V-Table
}

看到这里没有vtable,继续搜索发现TargetClassDescriptor有个别名ClassDescriptor。
在这里插入图片描述
全局搜索ClassDescriptor。在GenMeta里面就找到了一个类,ClassContextDescriptorBuilder。
在这里插入图片描述
往下看到了layout(布局)方法,这里先调用了super.layout方法,这里的super是TypeContextDescriptorBuilderBase。
在这里插入图片描述
在TypeContextDescriptorBuilderBase里面的layout调用了一系列方法,添加的和TargetClassDescriptor里面的属性就有点类似了。
在这里插入图片描述
而在调用了super.layout之后,ClassContextDescriptorBuilder的layout方法还调用了addVTable(),看到addVTable里面做了一系列操作,还计算了偏移量,把偏移量添加到了B,而这里B就是TargetClassDescriptor。最后遍历数组,添加了数组的指针也就是函数的指针。所以TargetClassDescriptor里面的size和vtable是这里面添加进去的。
在这里插入图片描述

2.4 macho

Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常⻅的 .o,.a .dylib Framework,dyld .dsym。
将工程的可执行文件拖入到machO打开。
在这里插入图片描述

文件头

header是头文件,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排,记录一些二进制信息。

  • Magic number: 标志着是32位还是64位的,这里是64位的。
  • CPU Type:cpu类型,这里可以看到是ARM64
  • CPU Subtype:cpu分类型,这里是ARM64_ALL
  • File Type: 文件类型,这里看到是可执行文件。
  • number of load Commands: 需要加载的命令个数,这里是35个
  • size of load Commands: 需要加载命令的大小
  • flags: 标识

在这里插入图片描述

Load Commands

在看到Load Commands,Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
在这里插入图片描述

Data 区

Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
在这里插入图片描述
例如text存指令:
在这里插入图片描述
cstring存硬编码的字符串:
在这里插入图片描述
对于swift的类,结构体和enum,都存在swift5_types里面,而这里面存的是类,结构体和enum的descriptor的地址信息。
在这里插入图片描述

验证一下 ,这里将14 FC FF FF 这四个字节 加上0000BC78这四个字节,得出来的就是descriptor在macho文件的内存地址。那么0XFFFFFC18 加0XBC88 得到 0x10000B88C。
在这里插入图片描述
之前在头文件看到虚拟内存地址从0x10000000开始的,那么B88C就是在data区的内存地址。
在这里插入图片描述
在这里插入图片描述
根据上面得到的TargetClassDescriptor结构可以知道,这里将地址平移13个4字节得到VTable,那么0000D8C0就是teach1方法的地址。
在这里插入图片描述
那么如何得到teach方法在程序内存运行时的地址呢?这里就需要将teach方法的地址加上ASLR(随机偏移地址)。接下来来到Xcode运行程序,然后打印image list 获得ASLR地址0x0000000100d1c000,加上teach方法地址B8C0得到0x100D278C0。
在这里插入图片描述
方法在底层的结构体是这个样子的,那么要得到impl,就需要位移4个字节,0x100D278C0 + 4 = 0x100D278C4.
在这里插入图片描述
impl存储的是相对指针,存储的是offset,所以需要将得到的impl地址加上offset,才能得到函数的地址。
那么 0x100D278C4 + 0XFFFFBAC0 得到0x200D23384, 那么在减去基地址,就得到0x100D23384
在Xcode里面读取寄存器X8,发现地址是正确的。
在这里插入图片描述
那么为什么Vtable在descriptor后面呢?在initClassVTable方法里面看到这里先获得了VTable的描述,然后在用vtableOffset + i 进行存储。vtableOffset是在程序运行时就已经确定了的字段。

在这里插入图片描述
那么struct的方法调度是什么样的呢?将LGTeacher改成struct后运行,发现直接就拿到了内存地址,所以就是一个直接的地址调用,是静态派发,这也就意味着在编译链接之后,地址就已经确定了。struct是值类型,那么struct没有继承关系,那么struct的函数就是属于自己的,没有必要在开辟一个连续的空间来记录方法,所以编译器直接优化成了地址调用。
在这里插入图片描述
既然有ClassContextDescriptorBuilder,那么就应该有StructContextDescriptorBuilder。在源码中找到StructContextDescriptorBuilder,看到这有个addLayoutInfo方法,可以看到这里没有vtable。所以对结构体来说,在编译链接之后,函数的地址就已经确定了
在这里插入图片描述
那么在extension里面的函数是怎么样的呢?

extension LGTeacher {
    func teach3() {
        print("teach3")
    }
}

看到了这里extension也是静态派发。
在这里插入图片描述
那么类的extension是什么派发呢?将struct改成class后运行发现也是静态派发。

在这里插入图片描述
那么为什么类里面的extension也是静态派发呢?猜测一下原因可能是这样的:创建类的时候,Vtable的大小已经确认好了,那么extension里面的方法需要插入到之前创建Vtable里面,那么Vtable的大小就不够了,并且这样的操作也比较消耗性能,所以这里extension就使用静态派发了。并且如果这个类作为其他类的父类的话,那么子类就需要记住父类VTable的位置,然后插入到父类的VTable中,这样的操作是比较复杂的,这也是使用静态派发的原因之一。
在这里插入图片描述

方法调度方式总结:
在这里插入图片描述

3. 影响函数派发方式

  • final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可⻅。在实际开发中不需要被重载的属性和方法可以添加final关键字。
final func teach() {
        print("teach")
    }

在这里插入图片描述

  • dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是原来的派发方式。动态性是可以对函数进行替换。
    下面的例子中,调用teach就变成了调用teach3.
    在这里插入图片描述

  • @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。

 @objc func teach() {
        print("teach")
    }

在这里插入图片描述

  • @objc + dynamic: 消息派发的方式,这样就可以使用method——swizzling
 @objc dynamic func teach() {
        print("teach")
    }

在这里插入图片描述

4. 函数内联

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

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内
    联函数作为优化。
  • always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
  • never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
  • 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))

在项目中或多或少都会接触到,看到这里的优化等级(Optimization Level)。这里面OC的是Cland,swift的是 Swift Compiler。
在这里插入图片描述
对于函数sum来说

int sum(int a, int b) {
    return a+b;
}

如果优化等级是None,那么这里就会调用函数。
在这里插入图片描述
如果优化等级是Smallest,Fastest,那么这里就会使用方法的内容替换直接调用该方法,从而优化性能。
在这里插入图片描述
如果对象只在声明的文件中可⻅,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得 对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明 中访问)
将Swift compiler 的优化等级改为for speed 后创建一个LGPerson类,添加下面方法和属性。

class LGPerson {
    private var sex: Bool
    private func updateSex() {
        self.sex = !self.sex
    }
    init (innerSex: Bool){
        self.sex = innerSex
    }
    func test(){
        self.updateSex()
    }
}

let t = LGPerson(innerSex: true)
t.test()

这个时候打下断点后运行,发现没有断住。这是因为在调用test的时候,发现没有意义,只是调用updateSex,所以把test符号优化掉了。而检查了LGPerson,发现没有继承,那么在默认就为updateSex添加 final 的标识,所以updateSex也被优化了。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值