Swift方法学习探究

272 阅读5分钟

一、异变方法

1.1 mutating关键字

Swift 中 class 和 struct 都能定义方法。但是有一点区别的是默认情况 下,值类型属性不能被自身的实例方法修改。

image.png

解决方式:方法用mutating关键字进行修饰就不报错了


struct Point {

var x = 0.0, y = 0.0

mutating func moveBy(x deltax: Double,y deltaY:Double) {

x += deltax

y += deltaY

}

}

1.2 sil分析

添加测试代码


import Foundation

struct Point {

var x = 0.0, y = 0.0

func test(){

let z = self.x

}

mutating func moveBy(x deltax: Double,y deltaY:Double) {

x += deltax

y += deltaY

}

}

在命令行中调用swiftc -emit-sil命令来生成sil文件,查看中间代码

swiftc -emit-sil main.swift

image.png

过 sil 来对比一下,不添加 mutating 访问和添加 mutating 两者有什么本质的区别


// Point.test()

sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {

...

} // end sil function '$s4main5PointV4testyyF'


// Point.moveBy(x:y:)

sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {

...

} // end sil function '$s4main5PointV6moveBy1x1yySd_SdtF'

对比两个函数可以发现test隐藏参数是Point,moveBy的隐藏参数是 @inout Point,关于@inout的官方解释:


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

用 @inout 修饰接受的是一个地址就,是可以修改的,否则接受的就是一个值,就不能够被修改。

1.3 输入输出参数 inout

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

如果形参没有被 inout 修饰,修改形参就会报错:

image.png

想要修改形参,需要用 inout 进行修饰,并传入地址类型:


var age = 2

func changeAge(_ age: inout Int) {

age += 10

}

print(age)

changeAge(&age)

print(age)

打印结果

2

12

二、方法调度

2.1 class方法调度
2.1.1 示例

class SwiftMethodViewController: UIViewController {

override func viewDidLoad() {

super.viewDidLoad()

let t = CHTeacher()

t.teach()

t.teach1()

t.teach2()

}

}

class CHTeacher{

func teach(){

print("teach")

}

func teach1(){

print("teach1")

}

func teach2(){

print("teach2")

}

}

汇编调试:

image.png

  • 上图中#0x50,0x58,0x60是三个连续的偏移地址,分别表示方法teach()、teach1()、teach2()

  • 当前断点指在t.teach2(),读取当前寄存器x8,如下


(lldb) register read x8

x8 = 0x0000000102ed8968 SwiftDemo`SwiftDemo.CHTeacher.teach2() -> () at SwiftMethodViewController.swift:33

2.1.2 sil验证

打开命令行,调用下面命令生sil


swiftc -emit-silgen -Onone -target x86_64-apple-ios14.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) 文件地址

sil中,定位到最后面的vtable函数表,其中有类所有的函数


sil_vtable CHTeacher {

#CHTeacher.teach: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC5teachyyF // CHTeacher.teach()

#CHTeacher.teach1: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach1yyF // CHTeacher.teach1()

#CHTeacher.teach2: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach2yyF // CHTeacher.teach2()

#CHTeacher.init!allocator: (CHTeacher.Type) -> () -> CHTeacher : @$s25SwiftMethodViewController9CHTeacherCACycfC // CHTeacher.__allocating_init()

#CHTeacher.deinit!deallocator: @$s25SwiftMethodViewController9CHTeacherCfD // CHTeacher.__deallocating_deinit

}

2.2 class的extension函数调用


extension CHTeacher {

func teach3() {

print("teach3")

}

}

汇编调试:

image.png

可以看到,class的extension函数teach3是直接地址调用,静态派发

2.3 struct函数调度

class SwiftMethodViewController: UIViewController {

override func viewDidLoad() {

super.viewDidLoad()

let t = CHTeacher()

t.teach()

t.teach1()

t.teach2()

}

}

struct CHTeacher{

func teach(){

print("teach")

}

func teach1(){

print("teach1")

}

func teach2(){

print("teach2")

}

}

汇编调试:

image.png

通过汇编代码可以看到,struct的函数调用是直接地址调用,静态派发

2.4 struct的extension函数调度

extension CHTeacher {

func teach3() {

print("teach3")

}

}

汇编调试:

image.png

可以看到,struct的extension函数teach3是直接地址调用,静态派发

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

三、关键字对派发的影响

3.1 final

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


class CHTeacher{

final func teach(){

print("teach")

}

...

}

汇编调试:

image.png


sil_vtable CHTeacher {

#CHTeacher.teach1: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach1yyF // CHTeacher.teach1()

#CHTeacher.teach2: (CHTeacher) -> () -> () : @$s25SwiftMethodViewController9CHTeacherC6teach2yyF // CHTeacher.teach2()

#CHTeacher.init!allocator: (CHTeacher.Type) -> () -> CHTeacher : @$s25SwiftMethodViewController9CHTeacherCACycfC // CHTeacher.__allocating_init()

#CHTeacher.deinit!deallocator: @$s25SwiftMethodViewController9CHTeacherCfD // CHTeacher.__deallocating_deinit

}

3.2 dynamic

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


class CHTeacher{

dynamic func teach(){

print("teach")

}

}

extension CHTeacher {

@_dynamicReplacement(for: teach())

func teach3(){

print("teach3")

}

}

let t = CHTeacher()

t.teach()

打印结果:teach3

3.3 @objc

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


class CHTeacher{

@objc func teach(){

print("teach")

}

}

3.4 @objec+dynamic

消息派发的方式,这时我们可以使用runtime的api


class SwiftMethodViewController: UIViewController {

override func viewDidLoad() {

super.viewDidLoad()

let t = CHTeacher()

t.teach()

}

}

class CHTeacher{

@objc dynamic func teach(){

print("teach")

}

}

汇编调试:

image.png

四、函数内联

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

4.1 OC函数内联

int sum(int a, int b) {

return a + b;

}

int main(int argc, char * argv[]) {

int a = sum(1,2);

NSLog(@"%d",a);

return 0;

}

汇编调试:

image.png

汇编看到,没有优化时,将1和2分别存入w0和w1,再调用sun函数

然后开启优化

image.png

再进行汇编调试:

image.png

开启优化后,直接将结果3存入寄存器,然后调用NSLog函数

4.2 Swift函数内联
  • Swift 中的内联函数是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。

// 编译器会认为 test 没有太多意义,会省略test的符号调用,直接调用print

func test() {

print("test");

}

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

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

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

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