背景
我们知道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
示例代码
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 分为raw 和canonical
- 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
- 类型擦除
- init_existential_addr 指令初始化了一个容器,该容器包含了实例(实现协议的对象)的引用。
- 通过 open_existential_addr 获取到上述容器,完成了类型擦除。在之后 SIL 访问都是 @opened("XXX") Animal 这一具体的协议类型。
- 方法调用
- 通过 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对函数进行调用
到这里就完毕了,感谢您的阅读,欢迎阅读我的其他文章!