Swift中的函数调用

Posted 想名真难

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift中的函数调用相关的知识,希望对你有一定的参考价值。

在了解Swift发布调用机制之前,先来了解下swift方法是如何保存的。

在swift中所有数据类型的只有两种:值类型,引用类型

值类型 : 在内存中直接保存值,没有引用计数;

引用类型 : 保存指针地址,在堆中分配内存

编程语言 函数调用机制有三种:

  • 直接调用
  • 函数表调用
  • 消息派发机制调用

函数调用机制是程序判断使用哪种途径去调用一个函数的机制,每次函数调用时都会被触发。
了解函数的调用机制,对于写出高性能代码来说十分有必要。
Java:默认函数表调用,可以通过final修饰修改成直接派发。
C++:默认是直接调用,但可以通过virtual修饰符来改成函数表派发
Objective-C:总是使用消息派发的机制,但允许开发者使用C直接派发来获取性能的提高。

Swift 函数调用机制,以上 三种都会涉及到。

调用方式:(Types of Dispatch)

程序派发的目的是为了告诉CPU需要被调用的函数在哪里。在我们深入理解Swift派发机制之前,先来了解下这三种派发机制,以及没中方式在动态性和性能之间的取舍。

直接调用:

直接派发是最快的,函数位置确定,调用需要的指令集少,并且编译器还有很大的优化空间(比如:函数内联)。直接派发也称为静态调用。
然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承。

函数表调用

函数表派发是编译型语言实现动态行为最常见的实现方式。函数表使用了一个数组来储存类声明的每一个函数的指针。大部分语言把这个称为『virtual table』虚函数表,Swift里称为 『witness table』。每一个类都会维护一个虚函数表,里边记录着当前类的所有函数,如果子类重写父类的方法, 表里只会保存override之后的函数地址,子类新增后会被插到这个数组的最后,运行时会根据这一个表决定实际要被调用的函数。
当一个函数调用时,首先要读取当前类的函数表,在读取函数对应的索引,然后跳转。

 消息派发机制调用

消息机制是调用函数的最动态的方式,也是Cocoa的基础,这样的机制催生了KVO、UIAppearence、CoreData等功能,这种运作方式的关键在于开发者可以在运行时改变函数的行为,不止可以通过swizzling 来改变,还可以用 isa-swizzling修改对象的继承关系,可以在面向对象的基础上实现自定义派发。

Struct方法调用

示例代码如下:

 通过调试信息可知,内存中只存储了变量age,并未存储方法。

 打开汇编堆栈,可知funcDemo方法的地址0x10a242430在代码段中,其在符号表中的名称为s11SwiftMethod9StructOneV8funcDemoyyF

 (注:cat address指令需要安装lldb扩展,点击这里下载

使用image list命令获取到系统默认的地址偏移量0x000000010a23f000

 使用方法地址0x10a242430减去默认的地址偏移量0x000000010a23f000,得到该方法真实的地址0x3430

此时查看程序的MachO文件,在TEXT段中查看0x3430,可以看出右图框中的部分即为方法funcDemo的汇编代码。

 再点击Xcode的step into按钮进入方法内部并查看其汇编代码。

 通过比较二者的汇编代码,可知此时程序执行的就是预先生成好的汇编代码。

再将在MachO文件的符号表中进行查找s11SwiftMethod9StructOneV8funcDemoyyF,也可以知道其地址为0x3430

 由此可以看出结构体方法是在程序编译链接时就直接在符号表中生成好,调用时无需额外操作。

按照同样的步骤,测试struct + protocol & struct + extension 也是直接调用。

枚举类方法调用

定义如下枚举类

 打开汇编调试

 由汇编指令可知,枚举类的方法是直接调用的。

Class 方法调用

class的方法调用最复杂,通过以下四种条件来决定方法是如何派发

  • 申明的位置, 全局函数,class中的函数,extension中的函数,protocol协议中的函数
  • 引用的类型, 是否继承于NSObject
  • 调用方式,OC调用swift方法, 还是swift内部调用swift方法
  • 优化特性,函数的关键字,final,@objc, dynamic

定义如下类:

class ClassMethodModel 
    func funcOne() 
        print("funcOne")
    
    func funcTwo() 
        print("funcTwo")
    

let cm = ClassMethodModel()
cm.funcOne()
cm.funcTwo()

使用 xcrun swiftc -emit-sil ClassMethod.swift 得到 swift代码 编译后的 sil 产物,可以看出class的方法是存在vtable中的。

sil_vtable ClassMethodModel 
  #ClassMethodModel.funcOne: (ClassMethodModel) -> () -> () : @$s11ClassMethod0aB5ModelC7funcOneyyF // ClassMethodModel.funcOne()
  #ClassMethodModel.funcTwo: (ClassMethodModel) -> () -> () : @$s11ClassMethod0aB5ModelC7funcTwoyyF // ClassMethodModel.funcTwo()
  #ClassMethodModel.init!allocator: (ClassMethodModel.Type) -> () -> ClassMethodModel : @$s11ClassMethod0aB5ModelCACycfC    // ClassMethodModel.__allocating_init()
  #ClassMethodModel.deinit!deallocator: @$s11ClassMethod0aB5ModelCfD    // ClassMethodModel.__deallocating_deinit

借助之前分析Struct和Enum的经验,先来看一下class的汇编调试代码

 从图中可以看出,rax即为变量 cm,0x45e9为funcOne的地址,0x45d9为funcTwo的地址。

对此可以得出结论:swift的class方法调用不是直接调用,而是

  1. 找到 Metadata ,类似于isa指针
  2. 确定函数地址(metadata + 偏移量)
  3. 执⾏函数

messageSend 调用

设计了如下Demo程序,模拟OC调用Swift代码。

 查看其断点处的汇编代码:

 可知OC调用swift @objc方法是通过objc_msgSend进行的。

反过来使用swift调用swift @objc方法,Demo代码如下:

 其断点处的汇编代码为:

根据汇编代码,结合上文分析class方法调用的汇编代码,可知swift调用swift @objc方法是函数表调用。

调用方式总结

关键字对函数调用的影响

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

  • 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()
        
    
    //被动态替换了
    //打印:teach5
  • @objc + dynamic: 让函数变成消息发送的机制,并且可以使用runtime中API,但是由于它是纯swift类,所以oc无法调用,如果继承至NSObject,则可以暴露给oc使用

函数内联

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

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

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

 访问权限

如果对象只在声明的⽂件中可⻅,可以⽤ 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

最终总结:

  • 值类型总是会直接派发,如结构体方法(Struct method), 枚举方法(Enum method)
  • extension扩展中的方法默认是不能被子类重写的,会编译报错,所以扩展种的方法调用属于静态派发
  • Protocol在初始化的申明中是采用函数表派发,扩展中使用静态派发
  • Class在初始化的申明中是采用函数表派发,扩展中使用静态派发
  • 继承于NSObject的Class在初始化的申明中是采用函数表派发,扩展中使用消息派发
  • 使用finial关键字修饰的函数采用直接调用,finial只能修改class中的方法,不能修饰struct/enum
  • 使用@objc修饰,只是表明此方法可以被OC调用,实际调用方式取决于在什么环境中调用,Swift中调用Swift为函数表调用 ,OC调用Swift为消息转发
  • @inline 只是编译器建议。如果生效,会在调用处替换方法调用为真正的方法实现,省略方法调用的过程。
Struct & EnumClass
普通方法直接调用函数表调用
protocol协议直接调用函数表调用
extension拓展直接调用直接调用
final-直接调用
继承方法-函数表调用
@objc-Swift调用Swift为函数表调用 ,OC调用Swift为消息转发
dynamic-函数表调用
@objc dynamic-objc_msgSend消息转发

Swift方法调用

Swift Method Dispatch

Swift 中的方法调用(Method Dispatch)

Swift中的函数调用

以上是关于Swift中的函数调用的主要内容,如果未能解决你的问题,请参考以下文章

为啥函数调用需要 Swift 中的参数名称?

应用程序中的调用函数didbecomeactive - Swift 2.0

使用完成处理程序(闭​​包)语法从objective-c文件调用swift文件中的函数

从 Swift 函数中的异步调用返回数据

从 Swift 函数中的异步调用返回数据

从 Swift 函数中的异步调用返回数据