iOS开发你需求知道Swift函数派发办法的

背景

咱们知道oc都是基于底层runtime音讯派发的,可是swift咱们知道是没有runtime的,那它的函数又是怎样派发呢,这一节咱们全面系统去探求下。

总结

废话不多说,先上总结!

iOS开发你需求知道Swift函数派发办法的

理论知识准备

Swift的编译进程

iOS开发你需求知道Swift函数派发办法的
Swift和OC都是用LLVM系统进行编译,只是对应的前端编译有差异,Swift是运用swiftc进行编译的,详细步骤如下:
iOS开发你需求知道Swift函数派发办法的

什么是SIL

SIL(SwiftIntermediate Language)称为swift中心言语,是Swift在编译进程中的中心产品, 经过 SIL 能够了解swift 底层的完结细节

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

本篇运用的指令如下:

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

点击我检查SIL其他指令

怎么生成SIL

示例代码

class Bird {
    dynamic func speak() {
        print("hello, i`m a small bird!")
    }
}
let bird = Bird()
bird.speak()

履行如下指令能够拿到SIL代码

swiftc -emit-silgen -Onone Cat.swift | xcrun swift-demangle >> Cat.sil

.sil文件首要包括如下几个方面

  • 类型的声明和界说,根本和原类的格局差不多,简单易懂
  • 代码块,首要调用逻辑相关
  • 函数表,存储函数信息

sil类型的声明和界说部分

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代码块部分

// 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函数表

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:调用值类型内部的函数

struct Dog {
    func speak() {
        print("汪汪")
    }
}
let dog = Dog()
dog.speak()

生成sil如下

// 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 中完结的办法

extension Person {
    func driver() {
        print("开车")
    }
}
class Person {
}
let person = Person()
person.driver()

sil文件信息如下:

iOS开发你需求知道Swift函数派发办法的
iOS开发你需求知道Swift函数派发办法的

  • 能够看到调用分类的函数,也是直接派发,经过function_ref找到函数地址然后运用apply指令进行调用
  • 一起函数表也不不会存在driver函数的信息

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

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

sil文件信息如下

iOS开发你需求知道Swift函数派发办法的
iOS开发你需求知道Swift函数派发办法的

  • 能够看到当函数被final润饰,也是直接派发,经过function_ref找到函数地址然后运用apply指令进行调用
  • 一起函数表也不不会存在speak函数的信息

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

直接派发函数存储的方位

示例代码:

iOS开发你需求知道Swift函数派发办法的

运转

iOS开发你需求知道Swift函数派发办法的

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

检查Mach-O

iOS开发你需求知道Swift函数派发办法的

  • 这个地址是存储在Mach-0中的__text,也便是代码段中
  • 需求履行的汇编指令都在这儿

静态调用函数地址咱们知道是存储在__text段里,可是符号呢测验怎么来的呢

iOS开发你需求知道Swift函数派发办法的

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

符号表:

iOS开发你需求知道Swift函数派发办法的

直接派发总结

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

函数表派发

首要场景

场景1: 函数存在sil_vtable中

class Teacher {
    func teach() {
        print("教语文课")
    }
}
let teacher = Teacher()
teacher.teach()

sil信息如下:

// 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中

protocol Animal {
    func speak()
}
class Cat: Animal {
    func speak() {
        print("喵喵")
    }
}
let cat = Cat()
cat.speak()

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

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好像没起效果?

%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:

let cat: Animal = Cat()

sil信息如下:

// 注:
// %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怎么运用音讯派发呢,咱们持续探求…

@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办法的代码块,而内部的完结直接引证了对应的办法,经过直接派发的办法进行调用。

// @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关键字。

// 增加 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对函数进行调用

到这儿就结束了,感谢您的阅览,欢迎阅览我的其他文章!