Swift中的函数调用

5,214 阅读8分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

mutating&inout

我们都知道Swift中class和struct都能定义方法,但是他们直接也有亿点点区别,见代码:


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

let p = Point()
p .moveBy(x: 20.0, y: 10.0)

//wrong:Left side of mutating operator isn't mutable: 'self' is immutable(变异运算符的左侧是不可变的:“self”是不可变的)

分析一下

在Swift中,function都会有一个隐式参数self,因此x += deltaX 相当于 self.x = deltaX,由于struct在Swift中时值类型,因此其实例对象存储的 是属性的值,值是不可变的,并且值.值这也是不合法的写法,因此会报错 而class是引用类型,因此本来传递的就是对象的地址,因此不会存在这种问题

结论

值类型属性不可以被自身实例方法修改

在Swift中,提供有关键字mutating允许值类型属性可以被自身实例方法修改

见以下代码:

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

mutating

首先通过sil编译得到结构体声明部分

struct Point {
  @_hasStorage @_hasInitialValue var x: Double { get set }
  @_hasStorage @_hasInitialValue var y: Double { get set }
  func test()
  mutating func moveBy(x deltaX: Double, y deltaY: Double)
  init()
  init(x: Double = 0.0, y: Double = 0.0)
}

可以看出,struct默认有初始化器,然后我们找到方法test()和方法moveBy()

// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {}

// Point.moveBy(x:y:)
sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {}

其中point即我们刚说的隐式参数,即调用该方法的实例对象。可以看出,添加了mutatiwng会在point去前添加关键字@inout,通过查阅Swift sil语法文档

An @inout parameter is indirect. The address must be of an initialized object.(当前参数类型是间接的,传递的是已经初始化过的地址) 因此,添加mutating相当于传入隐式参数self的地址,自然可以对对象的属性进行修改

结论

异变⽅法的本质:对于变异⽅法, 传⼊的 self 被标记为 inout 参数。⽆论在 mutating ⽅法内部发⽣什么,都会影响外部依赖类型的⼀切。

inout

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

var x = 100
//int是值类型,其实例存放值,因此传入的方法的也是值
//形式参数是let,不可更改,添加inout,需要传入地址
func modifyX(y newY: inout Int) {
    newY += 1
}
modifyX(y:&x)
print(x)

swift的方法调度

在Objective-C中,是通过runtime的objc_msgsend进行方法(消息)的派发,那再Swift中时怎么的呢,见代码:

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

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        let t = LGTeacher()
        t.teach()
    }
}
//在t.teach()打一个断点,进入汇编

Tj3sRs.md.png 通过register read x8 Tj8SWd.md.png 一个地址8字节,x0的前8字节就是metadata,可以理解为isa

结论

teach函数的调⽤过程:找到 Metadata ,确定函数地址(metadata + 偏移量), 执⾏函数

再新增几个方法 Tj8ykD.md.png

Tj850f.md.png

可以看出,三个方法的偏移量在内存中地址是连续的并且相差8个字节(即一个地址),可以推断出,三个方法是连续并且放在一个表里的,因此可以理解为在swift中函数调度是基于函数表的派发

函数表的证明

sil方向:

编译得到Viewcontroller.sil,见图

TjY4n1.md.png

Vtable :函数表,包含所有函数

源码方向:

查看源码 GenMeta.cpp

之前我们知道Metadate的数据结构,那么V-table是存放在什么地方的呢

struct Metadata{
  var kind: Int
  var superClass: Any.Type
  var cacheData: (Int, Int)
  var data: Int
  var classFlags: Int32
  var instanceAddressPoint: UInt32
  var instanceSize: UInt32
  var instanceAlignmentMask: UInt16
  var reserved: UInt16
  var classSize: UInt32
  var classAddressPoint: UInt32
  var typeDescriptor: UnsafeMutableRawPointer
  var iVarDestroyer: UnsafeRawPointer
}
其中关注一下typeDescriptor,不管是 ClassStruct , Enum 都有⾃⼰
的 Descriptor ,就是对类的⼀个详细描述
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
}

Tjah8O.md.png Tja42D.md.png

也可以看出在创建类的时候,会向类的底层结构体添加vtable,并通过偏移量来找到并执行函数

MachO方向:

  • Mach-O: Mach Object文件格式的缩写 是Mac以及iOS上可执行文件的格式 类似于Windows上的PE(porttable executable:可移植可执行文件 linux上的ELF格式(executable and linking format:可执行文件和链接格式)

  • Mach-O文件类型分类: 1.Executable:应用可执行的二进制文件,如.m/.h文件经过编译后会生成对应的Mach-O文件 2.Dylib Library:动态链接库 3.Static Library:静态链接库 4.Bundle:不能被链接 Dylib,只能在运行使用dlopen()加载 5.Relocatable Object File:可重定向文件类型

  • Mach-O文件结构

    TjBSMD.png

通过MachOView可以查看app可执行文件

Tj0sKg.md.png

  • Header:快速确认Mach-O文件的基本信息,如运行环境,Load Commands概述。

  • Load commands是⼀张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。

    TjBofP.png

  • Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,⼀个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪⼀个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者⼀些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。

其实,通过分析可执行文件mach-O也可以看出方法是通过V-table函数表的方式添加在metadata后面的

如果改为结构体,则会发现

TjrF8P.md.png

发现是直接调用函数地址,属于静态派发

总结

TjDZkR.md.png

个人理解:所谓函数表派发,是在编译过程中,将方法动态的添加到类的metadata中,在执行过程中通过metadata+偏移量的方式寻找并执行 而静态派发,这函数地址编译时则固定,不会更改

extension虽然是类的扩展,不过仍然是静态派发,如果采用动态派发的,不仅要在父类的metadata将方法添加进vtable,更要将子类继承的父类方法重新移动至最前,这对内存开销十分的大,而且采用静态派发的方式,extension能让类更加权责分明,因为方法不会被继承

影响函数派发方式

  1. final:添加了 final 关键字的函数⽆法被重写,使⽤静态派发,不会vtable 中出现,且对 objc 运⾏时不可⻅。实际开发过程中属性,⽅法,类不需要被重载

    class LGTeacher{
      final func teach(){
            print("teach")
        }
        func teach1(){
            print("teach1")
        }
        func teach2(){
            print("teach2")
        }
    }
    
  2. dynamic: 函数均可添加 dynamic 关键字,为⾮objc类和值类型的函数赋予动态性,但派发⽅式还是不变的。一般和 @_dynamicReplacement(for:teach1)一起使用

    class LGTeacher{
        dynamic func teach1(){
            print("teach1")
        }
    }
    extension LGTeacher{
        @_dynamicReplacement(for:teach1)
        func teach5(){
            print("teach5")
        }
    }
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            let t = LGTeacher()
            t.teach1()
        }
    }
    //被动态替换了
    //打印:teach
    
  3. @objc:该关键字可以将Swift函数暴露给Objc运⾏时,依旧是函数表派发。

  4. @objc + dynamic: 让函数变成消息发送的机制,并且可以使用runtime中API,但是由于它是纯swift类,所以oc无法调用,如果继承至NSObject,则可以暴露给oc使用

    class LGTeacher{
       @objc dynamic func teach1(){
            print("teach1")
        }
    }
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            let t = LGTeacher()
            t.teach1()
        }
    }
    

    Tjgc4J.md.png

    TjRpsx.md.png

函数内联

函数内联 是⼀种编译器优化技术,它通过使⽤⽅法的内容替换直接调⽤该⽅法,从⽽优化性能。

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

我们可以在xcode中设置优化等级,一般默认即可

Tj5RoV.md.png

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

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

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        let t = LGPerson(sex: true)
        t.test()
    }
}
//被优化后,不会走test,直接调用updateSex