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() {}
	}
offset0xA00A0xB00B0xC00C
00x121A.method10x121A.method10x121A.method1
10x222B.method20x322C.method2
20x323C.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深入分析函数的派发机制的主要内容,如果未能解决你的问题,请参考以下文章

Swift 编译器中间码 SIL

Swift

iOS开发-Swift进阶之类对象属性!

swift protocol 见证容器 虚函数表 与 动态派发

Swift中的函数调用

Swift之深入解析异步函数async/await的使用与运行机制