Swift 派发机制
Posted CoderStar
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift 派发机制相关的知识,希望对你有一定的参考价值。
对于编译型语言来看,有主要三种类型的函数派发方式,分别为:
分析三种派发方式主要从性能及动态性两方面讨论,这两个特性相对而言是矛盾的,性能要求高,则动态性差,反之亦然,其中直接派发又被称为静态派发,函数表派发与消息派发称为动态派发,大多数语言都会支持上面派发方式的一种到多种。如
直接派发是三种形式里面最快速的,在编译时就确定了方法的调用地址,汇编代码中,直接跳到方法的地址执行,生成的汇编指令最少。
优点:编译器可以对这种派发方式进行更多优化,比如函数内联等。
缺点:缺乏动态性,无法实现继承等;
函数表是编译型语言常见的派发方式,函数表使用数组来存储类中声明的每个函数的指针。对于这个表,大部分语言叫 Virtual table(虚函数表)
。根据 Swift 编译生成的 SIL 文件分析,Swift 中存在两种函数表,其中协议使用的是 witness_table
(SIL 文件中名为 sil_witness_table),类使用的是 virtual_table
(SIL 文件中名为 sil_vtable)。
每一个类都会维护一个函数表,里面记录着类所有的函数,如果父类函数被 override,表里面只会保存被 override 之后的函数。一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数;
一个函数被调用时会先去读取对象的函数表(读取第一次),再根据类的地址加上该的函数的偏移量得到函数地址(读取第二次),最后跳到那个地址上去(跳转一次)。整个过程是两次读取一次跳转,比直接派发慢一些。
消息派发是动态性最强的派发方式,也是性能最差的一种方式;方法调用包装成消息,发给运行时(相当于中间人),运行时会找到类对象,类对象会保存类的数据信息,或通过父类查找,直到命中执行,如果没找到方法,抛出异常,运行时提供了很多动态的方法用于改变消息派发的行为,相比函数表派发有很强的动态性,由于运行时支持的功能很多,方法查找的过程比较长,所以性能比较低;
OC 消息派发过程在这不展开说,后续有博文专门说这个。
分析SIL文件,我们可以分析出Swift中派发方式的规律,关于SIL相关知识,可以参照该文 ios编译简析 。
本文只给出关键命令 swiftc main.swift -emit-sil | xcrun swift-demangle > main.sil
。
派发方式与 SIL 文件中关键指令对应关系
Swift 语言支持三种派发方式。采用何种方式跟以下四种因素相关:
直接派发 | 函数表派发 | 消息派发 | |
---|---|---|---|
NSObject | @nonobjc 或者 final 修饰的方法 | 声明作用域中方法 | 扩展方法及被 dynamic 修饰的方法 |
Class | 不被 @objc 修饰的扩展方法及被 final 修饰的方法 | 声明作用域中方法 | dynamic 修饰的方法或者被 @objc 修饰的扩展方法 |
Protocol | 扩展方法 | 声明作用域中方法 | @objc 修饰的方法或者被 objc 修饰的协议中所有方法 |
Value Type | 所有方法 | 无 | 无 |
其他 | 全局方法,staic 修饰的方法;使用 final 声明的类里面的所有方法;使用 private 声明的方法和属性会隐式 final 声明; |
通过该表格你大概就可以理解一下 Swift 语言中的一些限制了:
@inline(__always)
声明这个函数总是编译成 inline 的形式, 这个修饰符只对函数体过长这种不会被内联优化的情况生效,其他情况也不生效;检查继承关系,对某些没有标记 final 的类通过计算,如果能在编译期确定执行的方法,则使用直接派发。比如一个函数没有 override,Swift 就可能会使用直接派发的方式。内联除了可以提高运行效率这个优点之外,还有另外一个好处,将部分关键函数进行内联优化,可以增大逆向难度。
有一个技术的圈子与一群志同道合的朋友非常重要,来我的技术公众号及博客,这里只聊技术干货。
微信公众号:CoderStar
博客:CoderStar\'s Blog
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 派发机制的主要内容,如果未能解决你的问题,请参考以下文章