Swift 类与结构体(下)

1,704 阅读3分钟

Swift类与结构体(上)中了解了类与结构体的区别,并且分析了swift类的结构。接下来让我们窥探一下类与结构体中的方法。

1、异变方法 mutating

在 swift 中值类型的修改属性是会直接修改实例的内存,相当于修改自身的值,所以值类型的属性是不能被自身的方法修改的,需要加上 mutating 关键字修饰

截屏2022-01-26 14.09.34.png

为什么加上 mutating之后就可以修改自身的值呢?通过下面的代码来窥探一下 mutating 的原理

sil分析

通过下面命令,生成 sil 文件

swiftc -emit-sil ViewController.swift > ./ViewController.sil

//需要将 UIKit 相关内容编译成 sil 时使用
swiftc -emit-sil -target x86_64-apple-ios15.0-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ./ViewController.sil
struct StructSwagger{
    var sex: Bool = false
    var age: Int = 18
    func test(_ age:Int){
        var age = age
        age = 20
        print(age)
    }
    mutating func test1(_ newage:Int){
        age += newage
    }
}
  • test函数 截屏2022-01-26 14.22.33.png
  • test1函数 截屏2022-01-26 14.22.50.png

两个方法对比说明

这两个方法最明显的有以下两点

  • 两个函数的第一个框标记说明,实例函数默认传入 self 参数的类型有区别
    • test 传入的是StructSwagger类型,是实例对象本身
    • test1 传入的是@inout StructSwagger类型,`是实例对象的地址
  • 从两个函数的第二个框标记可以发现,函数底层声明的行参类型不同
    • inout参数会声明一个var常量
    • 普通的参数则都是let常量 这两种区别,可以通过如下伪代码表示
//test1  
let self = StructSwagger

//test1
var self = &StructSwagger

总结:值类型中的属性都是直接存储在实例中,所以在方法内部修改属性相当于修改 self,而 self 在非mutating 的实例方法中传入的是值本身,并且默认是通过 let 修饰,所以无法修改;在mutating 实例方法中传入的 self 被标记为 inout 参数,这里传入的 self 是传入了实例的指针,并且是用 var 来声明,所以才可以在内部修改值类型的属性。

2、类的实例方法

2.1 汇编验证

截屏2022-01-26 15.44.31.png 从 x0 开始分析,由于 x0 寄存器一般用来存储函数的返回值,所以 x0 可能是 __alloc_init函数的返回值,也就是实例对象,将断点设置在 mov x20, x0的位置,通过 register read 可以读取存储的值,得到确实是实例对象的 metaldata 所以 x0 ,x20 中都是类的实例对象

(lldb) register read x0
      x8 = 0x00000001005e8ab0  type metadata for Swagg3r.Swagger

再看 ldr x8 [x20] 获取了 x20 中的值读取到 x8 中 也是就说拿到实例对象的metadata 存储到 x8,最后通过 实例对象的metadata一个偏移量再获取到函数地址进行调用,通过上面的汇编分析可得三个框的位置即是三个方法调用的位置,我们可以清晰的看到,三个方法的偏移量为 0x50,0x58,0x60 是三个连续的位置

总结:类的实例方法是通过 metadata 然后通过一个偏移量找到方法地址进行调用

汇编常见指令

  • cbz: 和 0 比较,如果结果为零就转移(只能跳到后面的指令)
  • cbnz: 和非 0 比较,如果结果非零就转移(只能跳到后面的指令)
  • cmp: 比较指令
  • br: 跳转到某地址(无返回)
  • blr: 跳转到某地址(有返回)
  • ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中
  • mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址),如:
 mov x1, x0 将寄存器 x0 赋值到寄存器 x1 ˙中
  • add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中, 如:
 add x0, x1, x2 将寄存器 x1 加 x2 的值赋值到 x0 
  • sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中:
  sub x0, x1, x2 将寄存器 x1 减 x2 的值赋值到 x0 
  • and: 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中, 如:
and x0, x0, #0x1 将寄存器 x0 的值和常量 1 按位与后赋值到 x0 
  • orr: 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中, 如:
 orr x0, x0, #0x1 将寄存器 x0 和常量 1   按位或后赋值到x0 
  • str : 将寄存器中的值写入到内存中,如:
 str x0, [x0, x8] ; 将寄存器 x0 保存到栈内存 [x0 + x8] 
  • ldr: 将内存中的值读取到寄存器中,如:
 ldr x0, [x1, x2] 将寄存器 x1 和寄存器 x2 的值相加作为地址,去该内存地址的值存储到 x0 

2.2 sil验证

通过下面命令,生成 sil 文件

swiftc -emit-sil ViewController.swift > ./ViewController.sil

//需要将 UIKit 相关内容编译成 sil 时使用
swiftc -emit-sil -target x86_64-apple-ios15.0-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ./ViewController.sil

获取到 sil 的文件,可以看到文件最后有个 sil_vtable 的结构,里面存放了类的实例方法,由名称来看是应该是虚表,进一步验证了之前关于类的实例方法的猜想

截屏2022-01-26 15.59.27.png

3、结构体的方法调用

struct Swagger {
    func test() {
        print("test")
    }
    func test1() {
        print("test1")
    }
    func test2() {
        print("test2")
    }
}
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let t = Swagger()
        t.test()
        t.test1()
        t.test2()
    }
}

截屏2022-01-26 16.23.56.png 在 Swift 中,调用一个结构体的方法是直接拿到函数的地址直接调用,包括初始化方法。Swift 是一门静态语言,许多东西在运行的时候就可以确定了,所以才可以直接拿到函数的地址进行调用,这个调用的形式称作静态派发

4、其他函数调度方式

4.1 extension 函数调用

截屏2022-01-26 16.27.14.png

截屏2022-01-26 16.26.31.png

可以看到,无论是 class或者是struct 在extension 中的的方法都是通过静态派发的方式

4.2 继承自NSObject的类

用同样的方式验证得到继承自NSObject类的sil_vtable和纯swift的类一样,类的实例方法会存储在Vtable中,通过Vtable函数表派发,extension的方法不会存储,通过静态派发调用

4.3 方法调度总结

log1.jpeg

4.4 关键字对派发方式的影响

class classSwagger{
    final func finalSwag() { }

    static func staticSwag() { }

    dynamic func dynamicSwag() { }

    @objc func objcSwag() { }

    @objc dynamic func objcDynamicSwag() { }
}

截屏2022-01-26 14.18.34.png

  • final

添加了final关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且 对 objc 运行时不可⻅

  • static static 方法不会存在vTable中,也是通过静态派发
  • dynamic 为非 objc 类和值类型的函数赋予动态性,修饰的方法会存在vtable中,通过函数表派发调用
  • @objc 该关键字可以将 Swift 函数暴露给 objc 运行时,与 OC 交互,修饰的方法会存在vtable中,通过函数表派发调用
  • @objc + dynamic @objc + dynamic 就会变成消息派发的方式-也就是 OC 中的消息机制

5、内联函数

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

swift 内联

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

Xcode 设置 截屏2022-01-26 16.49.42.png

private 的优化操作

如果对象只在声明的文件中可⻅,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private: 定义的声明中访问)