Swift之从SIL深入分析函数的派发机制
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之从SIL深入分析函数的派发机制相关的知识,希望对你有一定的参考价值。
一、引言
- 现有如下代码,输出什么结果?
protocol Drawing {
func render()
}
extension Drawing {
func circle() { print("protocol") }
func render() { circle() }
}
class SVG: Drawing {
func circle() { print("class") }
}
SVG().render()
// what's the output?
- 运行结果是: protocol,这是因为 extension 中声明的函数是静态派发,编译的时候就已经确定了调用地址,类无法重写实现。
- 通过 SIL 分析一下:
swiftc -emit-silgen -O demo.swift -o demo.sil
- demo.sil 如下所示:
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = metatype $@thick SVG.Type // user: %4
// function_ref SVG.__allocating_init()
%3 = function_ref @$s4demo3SVGCACycfC : $@convention(method) (@thick SVG.Type) -> @owned SVG // user: %4
%4 = apply %3(%2) : $@convention(method) (@thick SVG.Type) -> @owned SVG // user: %6
%5 = alloc_stack $SVG // users: %10, %9, %8, %6
store %4 to [init] %5 : $*SVG // id: %6
// function_ref Drawing.render()
%7 = function_ref @$s4demo7DrawingPAAE6renderyyF : $@convention(method) <τ_0_0 where τ_0_0 : Drawing> (@in_guaranteed τ_0_0) -> () // user: %8
%8 = apply %7<SVG>(%5) : $@convention(method) <τ_0_0 where τ_0_0 : Drawing> (@in_guaranteed τ_0_0) -> ()
destroy_addr %5 : $*SVG // id: %9
dealloc_stack %5 : $*SVG // id: %10
%11 = integer_literal $Builtin.Int32, 0 // user: %12
%12 = struct $Int32 (%11 : $Builtin.Int32) // user: %13
return %12 : $Int32 // id: %13
} // end sil function 'main'
- 可以看到 SVG 初始化后,是直接调用 Drawing.render() 协议的静态函数的。
// Drawing.render()
sil hidden [ossa] @$s4demo7DrawingPAAE6renderyyF : $@convention(method) <Self where Self : Drawing> (@in_guaranteed Self) -> () {
// %0 "self" // users: %3, %1
bb0(%0 : $*Self):
debug_value_addr %0 : $*Self, let, name "self", argno 1 // id: %1
// function_ref Drawing.circle()
%2 = function_ref @$s4demo7DrawingPAAE6circleyyF : $@convention(method) <τ_0_0 where τ_0_0 : Drawing> (@in_guaranteed τ_0_0) -> () // user: %3
%3 = apply %2<Self>(%0) : $@convention(method) <τ_0_0 where τ_0_0 : Drawing> (@in_guaranteed τ_0_0) -> ()
%4 = tuple () // user: %5
return %4 : $() // id: %5
} // end sil function '$s4demo7DrawingPAAE6renderyyF'
- 而对于 Drawing.render() 来说,内部也只直接调用 Drawing.circle() 的,所以这是编译期就决定了的。
二、派发机制
① 静态派发
- 静态派发是三种派发方式中最快的,CPU 直接拿到函数地址并进行调用。编译器优化时,也常常将函数进行内联,将其转换为静态派发方式,提升执行速度。
- C++ 默认使用静态派发;在 Swift 中给函数加上 final 关键字,也会变成静态派发。
- 优点:使用最少的指令集,办最快的事情。
- 缺点:静态派发最大的弊病就是没有动态性,不支持继承。
② 函数表派发
- 编译型语言中最常见的派发方式,既保证了动态性也兼顾了执行效率。
- 函数所在的类会维护一个“函数表”(虚函数表),存取了每个函数实现的指针。
- 每个类的 vtable 在编译时就会被构建,所以与静态派发相比多出了两个读取的工作:
-
- 读取该类的 vtable;
-
- 读取函数的指针
- 优点:
-
- 查表是一种简单,易实现,而且性能可预知的方式。
-
- 理论上说,函数表派发也是一种高效的方式。
- 缺点:
-
- 与静态派发相比,从字节码角度来看,多了两次读和一次跳转;
-
- 与静态派发相比,编译器对某些含有副作用的函数无法优化;
-
- Swift 类扩展里面的函数无法动态加入该类的函数表中,只能使用静态派发的方式。
- 举一个示例:
class A {
func method1() {}
}
class B: A {
func method2() {}
}
class C: B {
override func method2() {}
func method3() {}
}
offset | 0xA00 | A | 0xB00 | B | 0xC00 | C |
---|---|---|---|---|---|---|
0 | 0x121 | A.method1 | 0x121 | A.method1 | 0x121 | A.method1 |
1 | 0x222 | B.method2 | 0x322 | C.method2 | ||
2 | 0x323 | C.method3 |
let obj = C()
obj.method2()
- 当 method2 被调用时,会经历下面的几个过程:
-
- 读取对象 0xC00 的函数表;
-
- 读取函数指针的索引, method2 的地址为 0x322;
-
- 跳转执行 0x322。
③ 消息派发
- 消息机制是调用函数最动态的方式。由于 Swfit 使用的依旧是 Objective-C 的运行时系统,消息派发其实也就是 Objective-C 的 Message Passing(消息传递)。
id returnValue = [obj messageName:param];
// 底层代码
id returnValue = objc_msgSend(obj, @selector(messageName:), param);
- 优点:
-
- 动态性高;
-
- Method Swizzling;
-
- isa Swizzling
-
- …
- 缺点:
-
- 执行效率是三种派发方式中最低的;
-
- 所幸的是 objc_msgSend 会将匹配的结果缓存到一个映射表中,每个类都有这样一块缓存。若是之后发送相同的消息,执行速率会很快。
三、Swift 的派发机制
- Swift 的派发机制受到 4 个因素的影响:
-
- 数据类型;
-
- 函数声明的位置;
-
- 指定派发方式;
-
- 编译器优化。
① 数据类型
类型 | 初始声明 | 扩展 |
---|---|---|
值类型 | 静态派发 | 静态派发 |
协议 | 函数表派发 | 静态派发 |
类 | 函数表派发 | 静态派发 |
NSObject子类 | 函数表派发 | 静态派发 |
- 现有如下测试代码:
class MyClass {
func testOfClass() {}
}
struct MyStruct {
func testOfStruct() {}
}
- 来看看 SIL 的结果:
class MyClass {
func testOfClass()
@objc deinit
init()
}
struct MyStruct {
func testOfStruct()
init()
}
// MyClass.testOfClass()
sil hidden [ossa] @$s4demo7MyClassC06testOfC0yyF : $@convention(method) (@guaranteed MyClass) -> () {
...
} // end sil function '$s4demo7MyClassC06testOfC0yyF'
// MyClass.deinit
sil hidden [ossa] @$s4demo7MyClassCfd : $@convention(method) (@guaranteed MyClass) -> @owned Builtin.NativeObject {
...
} // end sil function '$s4demo7MyClassCfd'
// MyClass.__deallocating_deinit
sil hidden [ossa] @$s4demo7MyClassCfD : $@convention(method) (@owned MyClass) -> () {
...
} // end sil function '$s4demo7MyClassCfD'
// MyClass.__allocating_init()
sil hidden [exact_self_class] [ossa] @$s4demo7MyClassCACycfC : $@convention(method) (@thick MyClass.Type) -> @owned MyClass {
...
} // end sil function '$s4demo7MyClassCACycfC'
// MyClass.init()
sil hidden [ossa] @$s4demo7MyClassCACycfc : $@convention(method) (@owned MyClass) -> @owned MyClass {
...
} // end sil function '$s4demo7MyClassCACycfc'
// MyStruct.testOfStruct()
sil hidden [ossa] @$s4demo8MyStructV06testOfC0yyF : $@convention(method) (MyStruct) -> () {
...
} // end sil function '$s4demo8MyStructV06testOfC0yyF'
// MyStruct.init()
sil hidden [ossa] @$s4demo8MyStructVACycfC : $@convention(method) (@thin MyStruct.Type) -> MyStruct {
...
} // end sil function '$s4demo8MyStructVACycfC'
sil_vtable MyClass {
#MyClass.testOfClass: (MyClass) -> () -> () : @$s4demo7MyClassC06testOfC0yyF // MyClass.testOfClass()
#MyClass.init!allocator: (MyClass.Type) -> () -> MyClass : @$s4demo7MyClassCACycfC // MyClass.__allocating_init()
#MyClass.deinit!deallocator: @$s4demo7MyClassCfD // MyClass.__deallocating_deinit
}
- 抛开函数具体的实现,可以看到:
-
- struct 类型仅使用静态派发,不存在 vtable 结构;
-
- class 类型存在 vtable 结构,函数依次被存放在 vtable 中,使用函数表派发。
② 函数声明的位置
- 函数声明位置的不同也会导致派发方式的不同:
-
- 在“类”中声明;
-
- 在“扩展”中声明。
protocol MyProtocol {
func testOfProtocol()
}
extension MyProtocol {
func testOfProtocolInExtension() {}
}
class MyClass: MyProtocol {
func testOfClass() {}
func testOfProtocol() {}
}
extension MyClass {
func testOfClassInExtension() {}
}
- SIL 的结果:
protocol MyProtocol {
func testOfProtocol()
}
extension MyProtocol {
func testOfProtocolInExtension()
}
class MyClass : MyProtocol {
func testOfClass()
func testOfProtocol()
@objc deinit
init()
}
extension MyClass {
func testOfClassInExtension()
}
// MyProtocol.testOfProtocolInExtension()
sil hidden [ossa] @$s4demo10MyProtocolPAAE06testOfC11InExtensionyyF : $@convention(method) <Self where Self : MyProtocol> (@in_guaranteed Self) -> () {
...
} // end sil function '$s4demo10MyProtocolPAAE06testOfC11InExtensionyyF'
// MyClass.testOfClass()
sil hidden [ossa] @$s4demo7MyClassC06testOfC0yyF : $@convention(method) (@guaranteed MyClass) -> () {
...
} // end sil function '$s4demo7MyClassC06testOfC0yyF'
// MyClass.testOfProtocol()
sil hidden [ossa] @$s4demo7MyClassC14testOfProtocolyyF : $@convention(method) (@guaranteed MyClass) -> () {
...
} // end sil function '$s4demo7MyClassC14testOfProtocolyyF'
// MyClass.deinit
sil hidden [ossa] @$s4demo7MyClassCfd : $@convention(method) (@guaranteed MyClass) -> @owned Builtin.NativeObject {
...
} // end sil function '$s4demo7MyClassCfd'
// MyClass.__deallocating_deinit
sil hidden [ossa] @$s4demo7MyClassCfD : $@convention(method) (@owned MyClass) -> () {
...
} // end sil function '$s4demo7MyClassCfD'
// MyClass.__allocating_init()
sil hidden [exact_self_class] [ossa] @$s4demo7MyClassCACycfC : $@convention(method) (@thick MyClass.Type) -> @owned MyClass {
...
} // end sil function '$s4demo7MyClassCACycfC'
// MyClass.init()
sil hidden [ossa] @$s4demo7MyClassCACycfc : $@convention(method) (@owned MyClass) -> @owned MyClass {
...
} // end sil function '$s4demo7MyClassCACycfc'
// protocol witness for MyProtocol.testOfProtocol() in conformance MyClass
sil private [transparent] [thunk] [ossa] @$s4demo7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW : $@convention(witness_method: MyProtocol) (@in_guaranteed MyClass) -> () {
...
} // end sil function '$s4demo7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW'
// MyClass.testOfClassInExtension()
sil hidden [ossa] @$s4demo7MyClassC06testOfC11InExtensionyyF : $@convention(method) (@guaranteed MyClass) -> () {
...
} // end sil function '$s4demo7MyClassC06testOfC11InExtensionyyF'
sil_vtable MyClass {
#MyClass.testOfClass: (MyClass) -> () -> () : @$s4demo7MyClassC06testOfC0yyF // MyClass.testOfClass()
#MyClass.testOfProtocol: (MyClass) -> () -> () : @$s4demo7MyClassC14testOfProtocolyyF // MyClass.testOfProtocol()
#MyClass.init!allocator: (MyClass.Type) -> () -> MyClass : @$s4demo7MyClassCACycfC // MyClass.__allocating_init()
#MyClass.deinit!deallocator: @$s4demo7MyClassCfD // MyClass.__deallocating_deinit
}
sil_witness_table hidden MyClass: MyProtocol module demo {
method #MyProtocol.testOfProtocol: <Self where Self : MyProtocol> (Self) -> () -> () : @$s4demo7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW // protocol witness for MyProtocol.testOfProtocol() in conformance MyClass
}
- 抛开函数具体的实现,可以看到:
-
- 声明在“协议”或者“类”中的函数是使用函数表派发的;
-
- 声明在“扩展”中的函数则是静态派发。
- 此外可以看到,MyClass 实现 MyProtocol 的 testOfProtocol 在 sil_witness_table 中的函数地址对应的实现。
// protocol witness for MyProtocol.testOfProtocol() in conformance MyClass
sil private [transparent] [thunk] [ossa] @$s4demo7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW : $@convention(witness_method: MyProtocol) (@in_guaranteed MyClass) -> () {
// %0 // user: %1
bb0(%0 : $*MyClass):
%1 = load_borrow %0 : $*MyClass // users: %5, %3, %2
%2 = class_method %1 : $MyClass, #MyClass.testOfProtocol : (MyClass) -> () -> (), $@convention(method) (@guaranteed MyClass) -> () // user: %3
%3 = apply %2(%1) : $@convention(method) (@guaranteed MyClass) -> ()
%4 = tuple () // user: %6
end_borrow %1 : $MyClass // id: %5
return %4 : $() // id: %6
} // end sil function '$s4demo7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW'
sil_witness_table hidden MyClass: MyProtocol module demo {
method #MyProtocol.testOfProtocol: <Self where Self : MyProtocol> (Self) -> () -> () : @$s4demo7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW // protocol witness for MyProtocol.testOfProtocol() in conformance MyClass
}
- 可以看到,通过 testOfProtocol 的具体实现 @$s4demo7MyClassCAA0B8ProtocolA2aDP06testOfD0yyFTW,在其内部还是执行的 MyClass 的 MyClass.testOfProtocol 函数。即:无论是通过协议,还是通过类进行访问,最终都访问的是 MyClass.testOfProtocol 函数。
③ 指定派发方式
- 给函数添加关键字 final 的修饰也会改变其派发方式。
- 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且对 objc 运行时不可见。
class Test {
final func foo() {}
}
Test().foo()
sil_vtable Test {
#Test.init!allocator: (Test.Type) -> () -> Test : @$s4demo4TestCACycfC // Test.__allocating_init()
#Test.deinit!deallocator: @$s4demo4TestCfD // Test.__deallocating_deinit
}
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = metatype $@thick Test.Type // user: %4
// function_ref Test.__allocating_init()
%3 = function_ref @$s4demo4TestCACycfC : $@convention(method) (@thick Test.Type) -> @owned Test // user: %4
%4 = apply %3(%2) : $@convention(method) (@thick Test.Type) -> @owned Test // users: %7, %6
// function_ref Test.foo()
%5 = function_ref @$s4demo4TestC3fooyyF : $@convention(method) (@guaranteed Test) -> () // user: %6
%6 = apply %5(%4) : $@convention(method) (@guaranteed Test) -> ()
destroy_value %4 : $Test // id: %7
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
return %9 : $Int32 // id: %10
} // end sil function 'main'
- final 关键字会将函数变为静态派发,不会在 vtable 中出现。从 main 函数中的调用 function_ref 也可以看得出。
- 函数均可添加 dynamic 关键字,为非 objc 类和值类型的函数赋予动态性,但派发方式还是函数表派发。
- 如下所示,展示如何利用 dynamic 关键字,实现 Method Swizzling:
class Test {
dynamic func foo() {
print("bar")
}
}
extension Test {
@_dynamicReplacement(for: foo())
func foo_new() {
print("bar new")
}
}
Test().foo() // bar new
- @objc:该关键字可以将 Swift 函数暴露给 ObjC 运行时,但并不会改变其派发方式,依旧是函数表派发。
class Test {
@objc func foo() {}
}
// Test.foo()
sil hidden [ossa] @$s4demo4TestC3fooyyF : $@convention(method) (@guaranteed Test) -> () {
// %0 "self" // user: %1
bb0(%0 : @guaranteed $Test):
debug_value %以上是关于Swift之从SIL深入分析函数的派发机制的主要内容,如果未能解决你的问题,请参考以下文章