从SIL看Swift函数派发机制

1,578 阅读4分钟

0. 引言

下面的代码输出什么?(摘自objc.io twitter的swift quiz)

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() 的,所以这是编译期就决定了的。

了解Swift函数的派发机制,有助于你理解Swift中函数的调用过程,解决一些 “莫名其妙” 的bug。

1. 派发机制

函数派发的三种类型

  • 静态派发
  • 函数表派发
  • 消息派发

1.1 静态派发

静态派发是三种派发方式中最快的。CPU 直接拿到函数地址并进行调用。编译器优化时,也常常将函数进行内联,将其转换为静态派发方式,提升执行速度。

C++ 默认使用静态派发;在 Swift 中给函数加上final关键字,也会变成静态派发。

优点:

  • 使用最少的指令集,办最快的事情。

缺点:

  • 静态派发最大的弊病就是没有动态性,不支持继承。

1.2 函数表派发

编译型语言中最常见的派发方式,既保证了动态性也兼顾了执行效率。

函数所在的类会维护一个“函数表”(虚函数表),存取了每个函数实现的指针。

每个类的 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被调用时,会经历下面的几个过程:

  1. 读取对象 0xC00 的函数表
  2. 读取函数指针的索引, method2 的地址为0x322
  3. 跳转执行 0x322

1.3 消息派发

消息机制是调用函数最动态的方式。由于 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 会将匹配的结果缓存到一个映射表中,每个类都有这样一块缓存。若是之后发送相同的消息,执行速率会很快。

2. Swift的派发机制

Swift的派发机制受到4个因素的影响:

  • 数据类型
  • 函数声明的位置
  • 指定派发方式
  • 编译器优化

2.1 数据类型

类型初始声明扩展
值类型静态派发静态派发
协议函数表派发静态派发
函数表派发静态派发
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 中,使用函数表派发。

2.2 函数声明的位置

函数声明位置的不同也会导致派发方式的不同。

  • 在 类 中声明
  • 在 扩展 中声明
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 实现 MyProtocoltestOfProtocolsil_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 我们可以看到,在其内部还是执行的MyClassMyClass.testOfProtocol 函数。

即,无论是通过协议,还是通过类进行访问,最终都访问的是 MyClass.testOfProtocol 函数。

2.3 指定派发方式

给函数添加关键字的修饰也会改变其派发方式。

  • 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

函数均可添加 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 %0 : $Test, let, name "self", argno 1 // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function '$s4demo4TestC3fooyyF'

// @objc Test.foo()
sil hidden [thunk] [ossa] @$s4demo4TestC3fooyyFTo : $@convention(objc_method) (Test) -> () {
// %0                                             // user: %1
bb0(%0 : @unowned $Test):
  %1 = copy_value %0 : $Test                      // users: %6, %2
  %2 = begin_borrow %1 : $Test                    // users: %5, %4
  // function_ref Test.foo()
  %3 = function_ref @$s4demo4TestC3fooyyF : $@convention(method) (@guaranteed Test) -> () // user: %4
  %4 = apply %3(%2) : $@convention(method) (@guaranteed Test) -> () // user: %7
  end_borrow %2 : $Test                           // id: %5
  destroy_value %1 : $Test                        // id: %6
  return %4 : $()                                 // id: %7
} // end sil function '$s4demo4TestC3fooyyFTo'

...

sil_vtable Test {
  #Test.foo: (Test) -> () -> () : @$s4demo4TestC3fooyyF	// Test.foo()
  #Test.init!allocator: (Test.Type) -> () -> Test : @$s4demo4TestCACycfC	// Test.__allocating_init()
  #Test.deinit!deallocator: @$s4demo4TestCfD	// Test.__deallocating_deinit
}

foo 函数依然在 vtable 中,暴露给 Objec 的函数 @objc Test.foo() 其内部也是调用的 foo 函数。

  • @objc + dynamic
class Test {
    dynamic func foo1() {}
    @objc func foo2() {}
    @objc dynamic func foo3() {}
}

let text = Test()
text.foo1()
text.foo2()
text.foo3()

sil_vtable Test {
  #Test.foo1: (Test) -> () -> () : @$s4demo4TestC4foo1yyF	// Test.foo1()
  #Test.foo2: (Test) -> () -> () : @$s4demo4TestC4foo2yyF	// Test.foo2()
  #Test.init!allocator: (Test.Type) -> () -> Test : @$s4demo4TestCACycfC	// Test.__allocating_init()
  #Test.deinit!deallocator: @$s4demo4TestCfD	// Test.__deallocating_deinit
}

vtable 中只有 foo1foo2 ,没有 foo3

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4demo4textAA4TestCvp            // id: %2
  %3 = global_addr @$s4demo4textAA4TestCvp : $*Test // users: %16, %12, %8, %7
  %4 = metatype $@thick Test.Type                 // user: %6
  // function_ref Test.__allocating_init()
  %5 = function_ref @$s4demo4TestCACycfC : $@convention(method) (@thick Test.Type) -> @owned Test // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick Test.Type) -> @owned Test // user: %7
  store %6 to [init] %3 : $*Test                  // id: %7
  %8 = load_borrow %3 : $*Test                    // users: %11, %10, %9
  %9 = class_method %8 : $Test, #Test.foo1 : (Test) -> () -> (), $@convention(method) (@guaranteed Test) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (@guaranteed Test) -> ()
  end_borrow %8 : $Test                           // id: %11
  %12 = load_borrow %3 : $*Test                   // users: %15, %14, %13
  %13 = class_method %12 : $Test, #Test.foo2 : (Test) -> () -> (), $@convention(method) (@guaranteed Test) -> () // user: %14
  %14 = apply %13(%12) : $@convention(method) (@guaranteed Test) -> ()
  end_borrow %12 : $Test                          // id: %15
  %16 = load_borrow %3 : $*Test                   // users: %19, %18, %17
  %17 = objc_method %16 : $Test, #Test.foo3!foreign : (Test) -> () -> (), $@convention(objc_method) (Test) -> () // user: %18
  %18 = apply %17(%16) : $@convention(objc_method) (Test) -> ()
  end_borrow %16 : $Test                          // id: %19
  %20 = integer_literal $Builtin.Int32, 0         // user: %21
  %21 = struct $Int32 (%20 : $Builtin.Int32)      // user: %22
  return %21 : $Int32                             // id: %22
} // end sil function 'main'

main 函数的调用来看,Test.foo1Test.foo2 都是通过 class_method 采用 函数表 的方式。而 Test.foo3 则是通过 objc_method 采用 消息派发 的方式。

  • @inline
class Test {
    @inline(__always) func foo() {}
}

sil_vtable Test {
  #Test.foo: (Test) -> () -> () : @$s4demo4TestC3fooyyF	// Test.foo()
  #Test.init!allocator: (Test.Type) -> () -> Test : @$s4demo4TestCACycfC	// Test.__allocating_init()
  #Test.deinit!deallocator: @$s4demo4TestCfD	// Test.__deallocating_deinit
}

告诉编译器将此函数静态派发,但将其转换成SIL代码后,依旧是 vtable 派发。

  • static
class Test {
    static 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 static Test.foo()
  %3 = function_ref @$s4demo4TestC3fooyyFZ : $@convention(method) (@thick Test.Type) -> () // user: %4
  %4 = apply %3(%2) : $@convention(method) (@thick Test.Type) -> ()
  %5 = integer_literal $Builtin.Int32, 0          // user: %6
  %6 = struct $Int32 (%5 : $Builtin.Int32)        // user: %7
  return %6 : $Int32                              // id: %7
} // end sil function 'main'

static 关键字会将函数变为静态派发,不会在 vtable 中出现。 从 main 函数中的调用 function_ref 也可以看得出。

  • 总结
类型静态派发函数表派发消息派发
值类型所有方法//
协议extension主体创建/
extension/final/static主体创建@objc + dynamic
NSObject子类extension/final/static主体创建@objc + dynamic

除此之外,编译器可能将某些方法优化为静态派发。例如,私有函数。

2.4 编译器优化

Swift 会尽可能的去优化函数派发方式。当一个类声明了一个私有函数时,该函数很可能会被优化为静态派发。

这也就是为什么在 Swift 中使用 target-action 模式时,私有 selector 会报错的原因(Objective-C 无法获取 #selector 指定的函数)。

另一个需要注意的是,NSObject子类 中的 属性,如果没有使用 dynamic 修饰的话, 这个优化会默认让 KVO 失效。因为,这个属性的 gettersetter 会被优化为静态派发。虽然,代码可以通过编译,不过动态生成的 KVO 函数就不会被触发。

3. 面试题2

下面的代码输出什么?

protocol Logger {
    func log(content: String)
}

extension Logger{
    func log(content: String){
        print(content)
    }
    func description()->String{
        return "Logger"
    }
}

class MyLogger:Logger{
    func log(content: String) {
        print("MyLogger: " + content)
    }
    func description()->String{
        return "MyLogger"
    }
}

let p1:Logger = MyLogger()
p1.log(content: "p1")
print(p1.description())
    
let p2:MyLogger = MyLogger()
p2.log(content: "p2")
print(p2.description())

答案:

let p1:Logger = MyLogger()
p1.log(content: "p1") 	// MyLogger: p1
print(p1.description()) // Logger
    
let p2:MyLogger = MyLogger()
p2.log(content: "p2")	// MyLogger: p2
print(p2.description()) // MyLogger

为什么会产生这种结果呢?

sil_vtable MyLogger {
  #MyLogger.log: (MyLogger) -> (String) -> () : @$s4demo8MyLoggerC3log7contentySS_tF	// MyLogger.log(content:)
  #MyLogger.description: (MyLogger) -> () -> String : @$s4demo8MyLoggerC11descriptionSSyF	// MyLogger.description()
  #MyLogger.init!allocator: (MyLogger.Type) -> () -> MyLogger : @$s4demo8MyLoggerCACycfC	// MyLogger.__allocating_init()
  #MyLogger.deinit!deallocator: @$s4demo8MyLoggerCfD	// MyLogger.__deallocating_deinit
}

sil_witness_table hidden MyLogger: Logger module demo {
  method #Logger.log: <Self where Self : Logger> (Self) -> (String) -> () : @$s4demo8MyLoggerCAA0C0A2aDP3log7contentySS_tFTW	// protocol witness for Logger.log(content:) in conformance MyLogger
}

/*  ⚠️**重点**  */
// protocol witness for Logger.log(content:) in conformance MyLogger
sil private [transparent] [thunk] [ossa] @$s4demo8MyLoggerCAA0C0A2aDP3log7contentySS_tFTW : $@convention(witness_method: Logger) (@guaranteed String, @in_guaranteed MyLogger) -> () {
// %0                                             // user: %4
// %1                                             // user: %2
bb0(%0 : @guaranteed $String, %1 : $*MyLogger):
  %2 = load_borrow %1 : $*MyLogger                // users: %6, %4, %3
  %3 = class_method %2 : $MyLogger, #MyLogger.log : (MyLogger) -> (String) -> (), $@convention(method) (@guaranteed String, @guaranteed MyLogger) -> () // user: %4
  %4 = apply %3(%0, %2) : $@convention(method) (@guaranteed String, @guaranteed MyLogger) -> ()
  %5 = tuple ()                                   // user: %7
  end_borrow %2 : $MyLogger                       // id: %6
  return %5 : $()                                 // id: %7
} // end sil function '$s4demo8MyLoggerCAA0C0A2aDP3log7contentySS_tFTW'

由于 Swift 是强类型的语言,所以 p1Logger 类型, p2MyLogger 类型。

protocol 通过 sil_witness_table 进行调用。sil_witness_table 中的 Logger.log 函数,内部实现为通过 class_method 调用 MyLogger.log ,所以无论是 p1 还是 p2 均正常输出 MyLogger: p1/p2

而对于 description 函数会有一些不同:

  • 对于 p1 Logger 类型来说,这是一个静态派发的函数,所以输出 Logger
  • 对于 p2 MyLogger 类型来说,这是一个 vtabel 派发的函数,所以输出 MyLogger

分析 main 函数,也可以得出相同的结论:

// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
	...
  // function_ref Logger.description()
  %26 = function_ref @$s4demo6LoggerPAAE11descriptionSSyF : $@convention(method) <τ_0_0 where τ_0_0 : Logger> (@in_guaranteed τ_0_0) -> @owned String // user: %27
  ...
  %63 = class_method %62 : $MyLogger, #MyLogger.description : (MyLogger) -> () -> String, 
} // end sil function 'main'
  • 对于 p1 Logger 类型来说,通过 function_ref 进行调用,为静态派发;
  • 对于 p2 MyLogger 类型来说,通过 class_method 进行调用,为函数表派发。

如果觉得本文对你有所帮助,给我点个赞吧~ 👍🏻