Swift 类的方法调度

2,161 阅读4分钟

Swfit结构体一文中我们介绍了,结构体方法的调度,接下来我们来探索swift中类对象方法调度。

VTable

SIL分析

首先我们先来看下如下代码

class Person {
    var age = 10
    func run(){
        print(1)
    }
    func walk(){
        print(2)
     
    }
    func play(){
        print(3)
    }
}

var p = Person()
p.run()
p.walk()
p.play()

定义了3个实例方法,我们先来看一下它的SIL代码长什么样子

swiftc -emit-sil main.swift | xcrun swift-demangle > ./main.sil && open main.sil
  • 将 main.swift 文件,编译为 SIL 文件,并且进行反混淆,并打开 main.sil 文件
class Person {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  func run()
  func walk()
  func play()
  @objc deinit
  init()
}

sil_vtable Person {
  #Person.age!getter: (Person) -> () -> Int : @main.Person.age.getter : Swift.Int	// Person.age.getter
  #Person.age!setter: (Person) -> (Int) -> () : @main.Person.age.setter : Swift.Int	// Person.age.setter
  #Person.age!modify: (Person) -> () -> () : @main.Person.age.modify : Swift.Int	// Person.age.modify
  #Person.run: (Person) -> () -> () : @main.Person.run() -> ()	// Person.run()
  #Person.walk: (Person) -> () -> () : @main.Person.walk() -> ()	// Person.walk()
  #Person.play: (Person) -> () -> () : @main.Person.play() -> ()	// Person.play()
  #Person.init!allocator: (Person.Type) -> () -> Person : @main.Person.__allocating_init() -> main.Person	// Person.__allocating_init()
  #Person.deinit!deallocator: @main.Person.__deallocating_deinit	// Person.__deallocating_deinit
}
sil @main : $@convention(c) 
	...
 %12 = class_method %9 : $Person, #Person.run : (Person) -> () -> (), $@convention(method) (@guaranteed Person) -> () // user: %13
  %13 = apply %12(%9) : $@convention(method) (@guaranteed Person) -> ()
  ...

  • sil_vtable是关键字, Person表示当前是Person的函数表
  • vtable中存储了所有的实例方法,包括属性的 gettersetter方法。
  • class_method: 查找 Person类的 run方法,然后直接调用

从SIL文件,我们可以看出 实例对象的所有方法都存放在vtable中。

源码分析

vtable是通过initClassVTable方法来创建的

static void initClassVTable(ClassMetadata *self) {
  const auto *description = self->getDescription();
  auto *classWords = reinterpret_cast<void **>(self);

  if (description->hasVTable()) {
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    auto descriptors = description->getMethodDescriptors();
    // 1, 将method 存入内存中
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
      auto &methodDescription = descriptors[i];
      swift_ptrauth_init(&classWords[vtableOffset + i],
                         methodDescription.Impl.get(),
                         methodDescription.Flags.getExtraDiscriminator());
    }
  }

  if (description->hasOverrideTable()) {
    auto *overrideTable = description->getOverrideTable();
    auto overrideDescriptors = description->getMethodOverrideDescriptors();

    for (unsigned i = 0, e = overrideTable->NumEntries; i < e; ++i) {
      auto &descriptor = overrideDescriptors[i];

      // Get the base class and method.
      auto *baseClass = cast_or_null<ClassDescriptor>(descriptor.Class.get());
      auto *baseMethod = descriptor.Method.get();

      // If the base method is null, it's an unavailable weak-linked
      // symbol.
      if (baseClass == nullptr || baseMethod == nullptr)
        continue;

      // Calculate the base method's vtable offset from the
      // base method descriptor. The offset will be relative
      // to the base class's vtable start offset.
      auto baseClassMethods = baseClass->getMethodDescriptors();

      // If the method descriptor doesn't land within the bounds of the
      // method table, abort.
      if (baseMethod < baseClassMethods.begin() ||
          baseMethod >= baseClassMethods.end()) {
        fatalError(0, "resilient vtable at %p contains out-of-bounds "
                   "method descriptor %p\n",
                   overrideTable, baseMethod);
      }

      // Install the method override in our vtable.
      auto baseVTable = baseClass->getVTableDescriptor();
      auto offset = (baseVTable->getVTableOffset(baseClass) +
                     (baseMethod - baseClassMethods.data()));

      swift_ptrauth_init(&classWords[offset],
                         descriptor.Impl.get(),
                         baseMethod->Flags.getExtraDiscriminator());
    }
  }
}
  • 1,通过for循环,从 i = 0开始,直到 VTableSize,将method依次读取到内存中 ,每个method的偏移量为 vtableOffset + i,在内存中是连续存放的,是一个数组结构。

extension

我们现在给Person类,扩展一个方法

class Person {
    var age = 10
    func run(){
        print(1)
    }
    func walk(){
        print(2)
     
    }
    func play(){
        print(3)
    }
}

extension Person {
    func study() {
        
    }
}

我们直接使用汇编来查看其调用 我们可以看出,对于 extension 里面的 func静态调度的。

final

使用 final修饰的方法,也是静态调度方法。

class Person {
    var age = 10
    final func run(){
        print(1)
    }
}

var p = Person()
p.run()

其汇编结果为

    0x1000035ef <+111>: leaq   -0x20(%rbp), %rdi
    0x1000035f3 <+115>: movq   %rax, -0x50(%rbp)
    0x1000035f7 <+119>: callq  0x100003d32               ; symbol stub for: swift_endAccess
    0x1000035fc <+124>: movq   -0x48(%rbp), %r13
    0x100003600 <+128>: callq  0x100003820               ; LYSwift.Person.run() -> () at main.swift:12

@objc

我们先将如下代码转化为SIL

class Person {
    @objc func walk(){
     
    }
}

var p = Person()
p.walk()

结果为

class Person {
  @objc func walk()
  @objc deinit
  init()
}
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
  ......
  %12 = class_method %9 : $Person, #Person.walk : (Person) -> () -> (), $@convention(method) (@guaranteed Person) -> () // user: %13
  %13 = apply %12(%9) : $@convention(method) (@guaranteed Person) -> ()
  strong_release %9 : $Person                     // id: %14
  ....
  return %16 : $Int32                             // id: %17
} 
// Person.walk()
sil hidden @main.Person.walk() -> () : $@convention(method) (@guaranteed Person) -> () {
// %0 "self"                                      // user: %1
bb0(%0 : $Person):
  debug_value %0 : $Person, let, name "self", argno 1 // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function 'main.Person.walk() -> ()'

// @objc Person.walk()
sil hidden [thunk] @@objc main.Person.walk() -> () : $@convention(objc_method) (Person) -> () {
// %0                                             // users: %4, %3, %1
bb0(%0 : $Person):
  strong_retain %0 : $Person                      // id: %1
  // function_ref Person.walk()
  %2 = function_ref @main.Person.walk() -> () : $@convention(method) (@guaranteed Person) -> () // user: %3
  %3 = apply %2(%0) : $@convention(method) (@guaranteed Person) -> () // user: %5
  strong_release %0 : $Person                     // id: %4
  return %3 : $()                                 // id: %5
} // end sil function '@objc main.Person.walk() -> ()'

sil_vtable Person {
  #Person.walk: (Person) -> () -> () : @main.Person.walk() -> ()	// Person.walk()
  #Person.init!allocator: (Person.Type) -> () -> Person : @main.Person.__allocating_init() -> main.Person	// Person.__allocating_init()
  #Person.deinit!deallocator: @main.Person.__deallocating_deinit	// Person.__deallocating_deinit
}
  • 1, 分别生成了两个函数Person.walk()@objc Person.walk()
  • 2,@objc Person.walk()中,实际调用Person.walk()

@objc关键字只是通过编译器给OC层提供一个函数调用的入口,标记暴露给OC,它修饰的方法不是静态调度的。如果想要在 OC 调用该方法,需要继承NSObject

dynamic

dynamic修饰的func,该方法就变成了一个动态方法,可以进行方法交换


class Person {

    dynamic func walk(){
        print("walk")
    }
}
extension Person {
    // 用 study  替换 walk
    @_dynamicReplacement(for: walk)
    func study() {
        print("study")
    }
}
var p = Person()
p.walk() // study
  • 被替换的方法必须使用dynamic修饰。
  • _dynamicReplacement(for:methodA)使用当前方法动态替换methodA

如果我们使用 @objc + dynamic 修饰函数,那么该方法就变成了消息发送

class Person {
    @objc dynamic func walk(){
        print("walk")
    }
}

var p = Person()
p.walk()

通过其汇编代码我们可以验证

0x100003bdc <+124>: movq   0x450d(%rip), %rsi        ; "walk"
0x100003be3 <+131>: movq   -0x48(%rbp), %rax
0x100003be7 <+135>: movq   %rax, %rdi
0x100003bea <+138>: callq  0x100003e28               ; symbol stub for: objc_msgSend

总结

swift类中方法存储在VTable中,在编译期间就会确定,extension中的方法不存放在VTable中,属于静态调用方法。final修饰的方法也是静态调用,@objc dynamic修饰的方法,调用时会进行消息转发