背景
咱们知道oc都是基于底层runtime音讯派发的,可是swift咱们知道是没有runtime的,那它的函数又是怎样派发呢,这一节咱们全面系统去探求下。
总结
废话不多说,先上总结!
理论知识准备
Swift的编译进程
Swift和OC都是用LLVM系统进行编译,只是对应的前端编译有差异,Swift是运用swiftc进行编译的,详细步骤如下:
什么是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
示例代码
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 分为raw和canonical
- 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文件信息如下:
- 能够看到调用分类的函数,也是直接派发,经过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文件信息如下
- 能够看到当函数被final润饰,也是直接派发,经过function_ref找到函数地址然后运用apply指令进行调用
- 一起函数表也不不会存在speak函数的信息
了解到直接派发有哪些场景和获取函数地址的办法,有个问题,没有找到函数存储方位,咱们再看看函数存储在哪
直接派发函数存储的方位
示例代码:
运转
- 能够发现是直接经过地址调用的
- 那么这个函数地址是存储在哪里的呢
检查Mach-O
- 这个地址是存储在Mach-0中的__text,也便是代码段中
- 需求履行的汇编指令都在这儿
静态调用函数地址咱们知道是存储在__text段里,可是符号呢测验怎么来的呢
- 在静态调用中,会看到关于这个地址的符号
- 地址咱们已经知道是存储在了__text中
- 那么这个符号存储在哪里呢,检查符号表
符号表:
直接派发总结
- 静态派发常见场景如下:
- 值类型目标的办法调用
- 引证类型被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
- 类型擦除
- 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怎么运用音讯派发呢,咱们持续探求…
@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对函数进行调用
到这儿就结束了,感谢您的阅览,欢迎阅览我的其他文章!