深入探索 Swift 中的方法及其调度

一、方法

1.1 方法的声明

我们知道,在 Swift 中, class 声明类类型, struct 声明结构体。类类型属于引用类型,结构体属于值类型。那么对于他们在方法上面的区别有什么体现?

 可以看到,同样的方法声明,结构体却会报错。这是为什么? 

Q:为什么如下加上关键字 mutating 就可以了

A: 因为值类型属性不能被自身的实例方法修改。

1.2 解读 SIL 代码

那我们可以通过 SIL 文件来看一下加上关键字的区别:

不加 mutating 关键字:

 加上 mutating 关键字:

可以看到,加上了关键字之后,对于最后一个参数(实际上就是 self)多了一个关键字 @inout。

An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)

也就是说,对于后者,方法中的自身(self)实际上传递的是已经初始化过的地址,而前者传递的是结构体本身。

用伪代码表示即为:

var p = Point()
//没有@inout 关键字
let x1 = p
//有@inout 关键字
var x2 = withUnsafePointer(to: &p){return $0}
p.x = 20

此时,x1 和 p 是两个独立的值,x2 就是 p 的指针地址,指向 p 这个值。所以实际上

x2.pointee = p

这就造就了 Swift 中的异变函数,异变函数的本质就是:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法 内部发生什么,都会影响外部依赖类型的一切。

同时,如果我们希望函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为 输入输出 形式参数 ,也就是加上 inout 关键字。

二、方法调度

0x10034763c <+28>:  mov    x0, #0x0
    0x100347640 <+32>:  bl     0x10034768c               ; type metadata accessor for LGSwift.LGTeacher at <compiler-generated>
    0x100347644 <+36>:  mov    x20, x0
    0x100347648 <+40>:  bl     0x1003475d0               ; LGSwift.LGTeacher.__allocating_init() -> LGSwift.LGTeacher at ViewController.swift:11
    0x10034764c <+44>:  mov    x20, x0
    0x100347650 <+48>:  str    x20, [sp, #0x8]
    0x100347654 <+52>:  mov    x0, x20
    0x100347658 <+56>:  bl     0x100349e5c               ; symbol stub for: swift_retain
    0x10034765c <+60>:  str    x20, [sp, #0x10]
->  0x100347660 <+64>:  ldr    x8, [x20]
    0x100347664 <+68>:  ldr    x8, [x8, #0x50]
    0x100347668 <+72>:  blr    x8
    0x10034766c <+76>:  ldr    x0, [sp, #0x8]
    0x100347670 <+80>:  bl     0x100349e50               ; symbol stub for: swift_release
    0x100347674 <+84>:  ldr    x0, [sp, #0x10]
    0x100347678 <+88>:  bl     0x100349e50               ; symbol stub for: swift_release
    0x10034767c <+92>:  ldp    x29, x30, [sp, #0x30]
    0x100347680 <+96>:  ldp    x20, x19, [sp, #0x20]
    0x100347684 <+100>: add    sp, sp, #0x40             ; =0x40 
    0x100347688 <+104>: ret 

相信这样的汇编代码大家都很眼熟,我们在开发过程中不止一次的跳转到这样的界面。下面我们就通过汇编代码的相关知识一起来看方法调度的流程。

首先,汇编代码中常用的指令如下:

汇编常见指令
mov

将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址)

add

将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中

sub

将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中

and

将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中

orr

将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中

str

将寄存器中的值写入到内存中

ldr

将内存中的值读取到寄存器中

cbz

和 0 比较,如果结果为零就转移(只能跳到后面的指令)

cbnz

和非 0 比较,如果结果非零就转移(只能跳到后面的指令)

cmp

比较指令

bl

(branch)跳转到某地址(无返回)

blr

跳转到某地址(有返回)

ret

子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中

2.1 类的方法调度

对于如下代码运行在真机上后,打断点:

//类
class Teacher {
    func teach() {
        print("teach")
    }
}

class ViewController: UIViewController{

    override func viewDidLoad() {
        var t = Teacher()
        t.teach()
    }
}

开启 Xcode->Debug->Debug Workflow-> Always Show Disassembly 后,断点会进入汇编代码如下:

我们在初始化和释放中间找到 blr 这一句。 就代表  t.teach()   这个方法的执行。

如果在 blr 这一句加上断点,并且按住 control 键点击 step into,就可以发现这一句代码进入的就是 teach() 这个方法。

通过红框内的汇编代码的解释如下:

1.bl         0x1041c35d0 :跳转到 Teacher 的初始化方法中。

2.mov     x20, x0 :将上一步初始化后的实例对象 x0 赋值给 x20(此时 x20 就是实例变量

3.str        x20 , [sp, #0x10] :将 x20的值保存到 sp + #0x10(偏移量)的内存处

4.ldr        x8, [x20]:取 x20 的值(第一个 8 字节,也就是 metadata)放入寄存器 x8 中

5.ldr        x8, [x8, #0x50]:将x8的值加上 0x50(偏移量)作为地址,将该地址的值放入寄存器 x8

6.blr        x8:此时 x8 就是 teach()这个函数的内存地址。通过 blr 跳转执行。

所以函数的调用过程:先找到 metadata ——> 确定函数地址(metadata+偏移量) ——> 执行函数基于函数表的调度。

2.1 结构体的方法调度

//结构体
struct Teacher {
    func teach() {
        print("teach")
    }
}

class ViewController: UIViewController{

    override func viewDidLoad() {
        var t = Teacher()
        t.teach()
    }
}

断点进入汇编的表示如下:

可以看到这里的方法调用就是直接的地址调用,也就是——静态派发

同样的,对于 extension 中声明的方法,他的调度方式也是静态派发

class Teacher {
    func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
}

extension Teacher {
    func teach2() {
        print("teach2")
    }
}

class ViewController: UIViewController{

    override func viewDidLoad() {
        var t = Teacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

在如上操作之后得到的汇编如下:

可以看到,这里的 teach2() 方法并不是通过 metadata + 偏移量的方式去寻找内存地址,而是直接通过内存地址去执行方法。

这是因为当代码编译到extension 这一步时,Teacher这个类的 V-Table 已经编译好了,所以没有办法再往里面找地址插入这个额外声明的方法,所以就将这个额外声明的方法直接存入内存地址中。

2.3 总结

类型调度方式 extension
值类型静态派发静态派发
函数表派发静态派发
NSObject 子类函数表派发静态派发

三、影响函数派发的方式

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

  • dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发

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

class Teacher {
    final func teach() {
        print("teach")
    }
    dynamic func teach1() {
        print("teach1")
    }
    @objc func teach2() {
        print("teach2")
    }
}

class ViewController: UIViewController{

    override func viewDidLoad() {
        var t = Teacher()
        t.teach()
        t.teach1()
        t.teach2()
    }
}

 另:对于 dynamic 修饰的方法。表示该方法为动态的。目的在于在外部可以调用  @_dynamicReplacement(for: )   来替换这个方法。比如:

class Teacher {
    dynamic func teach() {
        print("teach")
    }
}

extension Teacher {
    @_dynamicReplacement(for: teach)
    func teach3() {
        print("teach3")
    }
}

class ViewController: UIViewController{

    override func viewDidLoad() {
        var t = Teacher()
        t.teach()
    }
}

以上代码执行结果输出的结果是 "teach3"。

  • 一般来说,dynamic 会和 @objc 一起使用。他是消息派发方式。如:
class Teacher {
    @objc dynamic func teach() {
        print("teach")
    }
}

四、函数内联

在项目中,通过 Target——>Build Settings ——>Swift Compiler可以设置优化模式:

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

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内

    联函数作为优化。

  • always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为

  • never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。

  • 如果函数很⻓并且想避免增加代码段大小,可以使用@inline(never)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值