iOS开发你需要知道Swift函数派发方式的

背景

我们知道oc都是基于底层runtime消息派发的,但是swift我们知道是没有runtime的,那它的函数又是怎么派发呢,这一节我们全面系统去探究下。

总结

废话不多说,先上总结!

理论知识准备

Swift的编译过程

Swift和OC都是用LLVM体系进行编译,只是对应的前端编译有差异,Swift是使用swiftc进行编译的,具体步骤如下:

什么是SIL

SIL(Swift Intermediate Language)称为swift中间语言,是 Swift 在编译过程中的中间产物, 通过 SIL 可以了解swift 底层的实现细节

SIL is an SSA-form IR with high-level semantic information designed to implement the Swift programming language

本篇使用的指令如下:

shell 复制代码
swiftc -emit-silgen -Onone [源文件名字].swift | xcrun swift-demangle >> [产物文件名字].sil
  • swiftc -emit-silgen >> result.sil 来生成 SIL 文件
  • -Onone 告知编译器不要进行任何优化,有助于我们了解完整的细节
  • xcrun swift-demangle 命令将符号进行还原,增强 Swift 方法名、类型等符号的可读性

点击我查看SIL其他指令

如何生成SIL

示例代码

swift 复制代码
class Bird {
    
    dynamic func speak() {
        print("hello, i`m a small bird!")
    }
}

let bird = Bird()
bird.speak()

执行如下指令可以拿到SIL代码

shell 复制代码
swiftc -emit-silgen -Onone Cat.swift | xcrun swift-demangle >> Cat.sil

.sil文件主要包含如下几个方面

  • 类型的声明和定义,基本和原类的格式差不多,简单易懂
  • 代码块,主要调用逻辑相关
  • 函数表,存储函数信息

sil类型的声明和定义部分

swift 复制代码
sil_stage raw

import Builtin
import Swift
import SwiftShims

import Foundation

struct Bird {
  func speak()
  init()
}

@_hasStorage @_hasInitialValue let bird: Bird { get }

// bird
sil_global hidden [let] @Bird.bird : Bird.Bird : $Bird
  • sil_stage 分为rawcanonical
    • raw表示当前的 SIL 是未经优化的,我们设置为raw便于我们研究内部逻辑
    • canonical代表的则是优化后
  • 当前的声明逻辑和源码基本差异不大
  • 定义一个变量bird,sil global 说明这是一个全局变量,hidden 则代表当前变量只在当前模块可见。若将 Cat 和该变量声明为 public,则不会存在 hidden 关键字。

sil代码块部分

swift 复制代码
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  //1. 创建对象
  alloc_global @Bird.bird : Bird.Bird                // id: %2
  %3 = global_addr @Bird.bird : Bird.Bird : $*Bird   // users: %8, %7
  %4 = metatype $@thick Bird.Type                 // user: %6
  
  //2. 调用对象的初始化方法
  // function_ref Bird.__allocating_init()
  %5 = function_ref @Bird.Bird.__allocating_init() -> Bird.Bird : $@convention(method) (@thick Bird.Type) -> @owned Bird // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick Bird.Type) -> @owned Bird // user: %7
  store %6 to [init] %3 : $*Bird                  // id: %7
  %8 = load_borrow %3 : $*Bird                    // users: %11, %10, %9
  
  //3. 调用bird函数
  %9 = class_method %8 : $Bird, #Bird.speak : (Bird) -> () -> (), $@convention(method) (@guaranteed Bird) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (@guaranteed Bird) -> ()
  end_borrow %8 : $Bird                           // id: %11
  %12 = integer_literal $Builtin.Int32, 0         // user: %13
  %13 = struct $Int32 (%12 : $Builtin.Int32)      // user: %14
  return %13 : $Int32                             // id: %14
} // end sil function 'main'

第一步:分配内存空间

  • alloc_global 指令分配了全局变量 bird 所需要的内存空间,其类型为 Bird。
  • 通过 global_addr 读取该变量的内存地址,存入 %3 寄存器中。
  • metatype 指令获取 Cat 的元类型信息,存入 %4 寄存器中。

第二步:初始化实例

  • 通过 function_ref 指令,引用了 Bird.__allocating_init() 方法。
  • 紧接着通过 apply 指令执行 Bird.__allocating_init() 方法,创建出对应的实例,并存储到 %3 的内存地址上。

第三步:方法调用

  • 在完成了全局变量bird创建之后,SIL通过load_borrow指令从 %3 所存储的内存地址上读取对应的值。
  • 接着使用class_method 指令,查询实例对应的函数表,获取到需要执行的方法。
  • 最终调用apply方法完成方法调用。

sil函数表

swift 复制代码
sil_vtable Bird {
  #Bird.speak: (Bird) -> () -> () : @Bird.Bird.speak() -> ()	// Bird.speak()
  #Bird.init!allocator: (Bird.Type) -> () -> Bird : @Bird.Bird.__allocating_init() -> Bird.Bird	// Bird.__allocating_init()
  #Bird.deinit!deallocator: @Bird.Bird.__deallocating_deinit	// Bird.__deallocating_deinit
}
  • class 类型最常见的方法派发方式就是通过函数表派发,通过查询函数表里的方法后进行调用

了解了sil是什么。以及存在哪些核心内存提供我们分析,我们继续研究下具体的派发方式...

具体探究

直接派发

常见场景

场景1:调用值类型内部的函数

swift 复制代码
struct Dog {
    
    func speak() {
        print("汪汪")
    }
}

let dog = Dog()
dog.speak()

生成sil如下

swift 复制代码
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  ...
  
  // function_ref Dog.speak()
  %9 = function_ref @Dog.Dog.speak() -> () : $@convention(method) (Dog) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (Dog) -> ()
     ...
} // end sil function 'main'
  • 直接派发通过function_ref获取函数的地址
  • apply通过函数地址进行调用

场景2: extension 中实现的方法

swift 复制代码
extension Person {
    func driver() {
        print("开车")
    }
}

class Person {
   
}

let person = Person()
person.driver()

sil文件信息如下:

  • 可以看到调用分类的函数,也是直接派发,通过function_ref找到函数地址然后使用apply指令进行调用
  • 同时函数表也不不会存在driver函数的信息

场景3: 调用class引用类型内部被final修饰的函数

swift 复制代码
import Foundation

class Bird {
    
     final func speak() {
        print("hello, i`m a small bird!")
    }
}

let bird = Bird()
bird.speak()

sil文件信息如下

  • 可以看到当函数被final修饰,也是直接派发,通过function_ref找到函数地址然后使用apply指令进行调用
  • 同时函数表也不不会存在speak函数的信息

了解到直接派发有哪些场景和获取函数地址的方式,有个问题,没有找到函数存储位置,我们再看看函数存储在哪

直接派发函数存储的位置

示例代码:

运行

  • 可以发现是直接通过地址调用的
  • 那么这个函数地址是存储在哪里的呢

查看Mach-O

  • 这个地址是存储在Mach-0中的__text,也就是代码段中
  • 需要执行的汇编指令都在这里

静态调用函数地址我们知道是存储在__text段里,但是符号呢测试如何来的呢

  • 在静态调用中,会看到关于这个地址的符号
  • 地址我们已经知道是存储在了__text中
  • 那么这个符号存储在哪里呢,查看符号表

符号表:

直接派发总结

  • 静态派发常见场景如下:
    • 值类型对象的方法调用
    • 引用类型被final修饰的方法调用
    • 分类的方法调用
  • 调用方式是根据函数地址直接调用
  • 函数地址是存储在mach-o文件的__text段里
  • 函数的符号是取自mach-o文件的symbol_table

函数表派发

主要场景

场景1: 函数存在sil_vtable中

swift 复制代码
class Teacher {
    func teach() {
        print("教语文课")
    }
}

let teacher = Teacher()
teacher.teach()

sil信息如下:

swift 复制代码
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  ...
  %9 = class_method %8 : $Teacher, #Teacher.teach : (Teacher) -> () -> (), $@convention(method) (@guaranteed Teacher) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (@guaranteed Teacher) -> ()
  ...
} // end sil function 'main'

//sil_vtable函数表
sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @Teacher.Teacher.teach() -> ()	// Teacher.teach()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @Teacher.Teacher.__allocating_init() -> Teacher.Teacher	// Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @Teacher.Teacher.__deallocating_deinit	// Teacher.__deallocating_deinit
}

这种是调用引用类型内部的函数 ,函数存储在sil_vtable内部 ,通过class_method去函数表获取函数地址,通过apply进行函数调用

场景2: 函数存在sil_witness_table中

swift 复制代码
protocol Animal {
    func speak()
}

class Cat: Animal {
    func speak() {
        print("喵喵")
    }
}

let cat = Cat()
cat.speak()

sil信息如下: 将代码生成 SIL 之后,SIL 最底下函数表部分发现 Cat 类型多了一个 Witness Table,里面有我们协议中定义的 speak 方法。

swift 复制代码
sil_witness_table hidden Cat: Animal module Contents {
  method #Animal.speak: <Self where Self : Animal> (Self) -> () -> () : @protocol witness for Contents.Animal.speak() -> () in conformance Contents.Cat : Contents.Animal in Contents	// protocol witness for Animal.speak() in conformance Cat
}

这种调用Protocol内部的函数 ,函数会存放在sil_witness_table内部

可是当我们查看 main 函数中调用的指令,依然是通过 class_method 指令去获取方法,WTable 好像没起作用?

swift 复制代码
%9 = class_method %8 : $Cat, #Cat.speak : (Cat) -> () -> (), $@convention(method) (@guaranteed Cat) -> () // user: %10
%10 = apply %9(%8) : $@convention(method) (@guaranteed Cat) -> ()

这是因为 Swift 自动进行类型推导,cat 变量被推导成了 Cat 类型,而 WTable 只有类型为 Protocol 时才会使用。声明 cat 为 Animal,重新生成 SIL:

swift 复制代码
let cat: Animal = Cat()

sil信息如下:

swift 复制代码
// 注:
// %3 为 cat 实例的内存地址
// %6 为 cat 实例

// 1
%7 = init_existential_addr %3 : $*Animal, $Cat  // user: %8
store %6 to [init] %7 : $*Cat                   // id: %8
%9 = open_existential_addr immutable_access %3 : $*Animal to $*@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal // users: %11, %11, %10

// 2
%10 = witness_method $@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal, #Animal.speak : <Self where Self : Animal> (Self) -> () -> (), %9 : $*@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal : $@convention(witness_method: Animal) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // type-defs: %9; user: %11
%11 = apply %10<@opened("8ACFC95A-BFE1-11ED-B83F-56DB1A421F1A") Animal>(%9) : $@convention(witness_method: Animal) <τ_0_0 where τ_0_0 : Animal> (@in_guaranteed τ_0_0) -> () // type-defs: %9
  1. 类型擦除
    • init_existential_addr 指令初始化了一个容器,该容器包含了实例(实现协议的对象)的引用。
    • 通过 open_existential_addr 获取到上述容器,完成了类型擦除。在之后 SIL 访问都是 @opened("XXX") Animal 这一具体的协议类型。
  2. 方法调用
    • 通过 witness_method 查找协议的方法进行调用。
    • 获取到的方法的调用方式为:@convention(witness_method: Animal) ,代表该方法是在 WTable 表中的方法,需要通过函数表派发的方式执行。

函数表派发总结

  • 函数存储位置区分类型如下:
    • 引用类内部函数调用,函数存放sil_vtable内部
    • Protocal类型内部函数调用,函数存放在sil_witness_table内部
  • 调用方式区分类型如下:
    • 引用类内部函数调用,通过class_method去查找去
    • Protocal类型内部函数调用
      • 接受者是引用类型:还是使用class_method去查找去
      • 接受者是Protocal类型:
        • 类型擦除:根据对象新建一个容器、后续都是通过这个容器进行访问对象
        • 方法调用:通过opened调用容器对象,witness_method获取对象的协议方法,通过apply进行调用

消息派发

消息派发,也属于动态派发方式中的一种。我们最为熟悉 Objective-C 的方法都是通过消息派发的方式进行调用的。

swift如何使用消息派发呢,我们继续探究...

swift 复制代码
@objc
class Cat: NSObject {
    @objc func speak() {
        print("喵喵")
    }
}

let cat = Cat()
cat.speak()

// SIL
%9 = class_method %8 : $Cat, #Cat.speak : (Cat) -> () -> (), $@convention(method) (@guaranteed Cat) -> () // user: %10
%10 = apply %9(%8) : $@convention(method) (@guaranteed Cat) -> ()

查看SIL可以注意到,即使是添加@objc的方法,依然是通过函数表进行派发的。那添加@objc关键字它的作用体现在哪? 查看方法的代码块会发现,多了一个针对@objc方法的代码块,而内部的实现直接引用了对应的方法,通过直接派发的方式进行调用。

swift 复制代码
// @objc Cat.speak()
sil hidden [thunk] [ossa] @@objc Contents.Cat.speak() -> () : $@convention(objc_method) (Cat) -> () {
  // ...
  %3 = function_ref @Contents.Cat.speak() -> () : $@convention(method) (@guaranteed Cat) -> () // user: %4
  %4 = apply %3(%2) : $@convention(method) (@guaranteed Cat) -> () // user: %7
  // ...
}

因此仅仅是添加 @objc 关键字,不会影响方法的派发方式,只是生成了一个 OC 可见的版本。

而要让 Swift 的方法在运行时以消息派发的方式调用,还需要添加 dynamic 关键字。

swift 复制代码
// 添加 dynamic 关键字
@objc dynamic func speak() {}

// SIL
%9 = objc_method %8 : $Cat, #Cat.speak!foreign : (Cat) -> () -> (), $@convention(objc_method) (Cat) -> () // user: %10
%10 = apply %9(%8) : $@convention(objc_method) (Cat) -> ()

添加之后,方法由 objc_method 指令获取,同时方法被修饰为 @convention(objc_method), 表明该方法就是一个 OC 的方法,上述流程等价于 objc_msgSend()。同时 SIL 底部的 VTable 之中不会包含该方法。

函数存储位置 我们知道oc函数都是存储在class_rw_o内部,swift使用消息派发后函数也是存储在类对象的class_rw_o内部

消息派发总结

  • 触发条件
    • 类被@Objc修饰,函数被@objc dynamic修饰
  • 通过objc_method获取函数地址,通过apply对函数进行调用

到这里就完毕了,感谢您的阅读,欢迎阅读我的其他文章!

相关推荐
安和昂3 小时前
【iOS】知乎日报第三周总结
ios
键盘敲没电4 小时前
【iOS】知乎日报前三周总结
学习·ios·objective-c·xcode
B.-10 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
iFlyCai20 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
郝晨妤1 天前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc558886661 天前
iOS 18.1,未公开的新功能
ios
Hamm1 天前
先别急着喷,没好用的iOS-Ollama客户端那就自己写个然后开源吧
人工智能·llm·swift
CocoaKier1 天前
苹果商店下载链接如何获取
ios·apple
zhlx28351 天前
【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载
macos·ios·cocoa
XZHOUMIN2 天前
网易博客旧文----编译用于IOS的zlib版本
ios