Swift中的VTable简述

2021-06-18  本文已影响0人  佐佑卟份

在Swift中方法的调度分为静态方法直接调用与动态分派两种方式

  1. 静态方法
    静态方法表示其为不可变的,为了提高调用的效率苹果允许直接访问方法地址来调用该方法,比如说结构体中的方法
struct firstStruct {
    func test() {}
}
firstStruct().test()

断点在汇编可以看到其直接调用了该方法地址


0x1040d4640=ASLR+静态分析地址
  1. 动态派发
    除了静态方法外的其他方法都是通过VTable表查询的方式进行调用了,看一下VTable初始化过程,主要是本类的方法保存与父类的方法重载
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);
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i)
// 将本类中所有的方法存入到VTable表中
      classWords[vtableOffset + i] = description->getMethod(i);
  }

  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 = 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().data();
      auto offset = baseMethod - baseClassMethods;

      // Install the method override in our vtable.
// 将所有父类允许重载的方法全部加到本类的vtable中
      auto baseVTable = baseClass->getVTableDescriptor();
      classWords[baseVTable->getVTableOffset(baseClass) + offset]
        = descriptor.Impl.get();
    }
  }
}

在代码注释的位置看到,源码是通过遍历的方式,将本类中所有的可重载方法存入到VTable表中,循环写入在内存地址中的表现是连续存储的数据结构。因此可以根据地址偏移来取得对应的存储数据。
同样的,在方法后半部分是将父类中的所有可重载方法拷贝一份存入到本类对应的VTable中。
不难猜测Swift的方法派发效率会比OC的从父类查找要来的快,以空间换时间,提升了效率。

同样的通过查看源代码中VTable的查找方法可以简单做一下分析

void *
swift::swift_lookUpClassMethod(const ClassMetadata *metadata,
                               const MethodDescriptor *method,
                               const ClassDescriptor *description) {
  assert(metadata->isTypeMetadata());

  assert(isAncestorOf(metadata, description));

  auto *vtable = description->getVTableDescriptor();
  assert(vtable != nullptr);

  auto methods = description->getMethodDescriptors();
  unsigned index = method - methods.data();
  assert(index < methods.size());
// 根据方法描述取得该方法在vtable中的地址偏移
  auto vtableOffset = vtable->getVTableOffset(description) + index;
// 本身类的起始地址
  auto *words = reinterpret_cast<void * const *>(metadata);
// 得到动态方法的当前实际地址
  return *(words + vtableOffset);
}

调用方法是通过方法描述从VTable表中取得对应的偏移量,然后和本类的地址起始位置相加,就是得到该派发方法的实际地址。这个和取属性是类似的,都是实例起始地址 + 偏移量。

顶层的源代码调用已经很清晰的显示了VTable的创建与使用过程。通过SIL中间层代码来加强一下印象
先看下SIL对VTable的定义

// 其中单引号中是固定写法,sil_vtable someClassName { class中所有方法 }
decl ::= sil-vtable
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
// 每个方法存在vtable表中的内容都是    方法描述 : 方法名
sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name

这段的含义就是申明了sil-vtable的结构,对照示例来看:

class funcExampleClass {
    func test1() {}
    func test2() {}
    func test3() {}
}
let example = funcExampleClass()
example.test1()

对应的SIL源码,上述可知每个类都会调动initClassVTable方法来得到vtable表

class funcExampleClass {
  func test1()
  func test2()
  func test3()
  @objc deinit
  init()
}
......
// 此处"#"号并不是注释的意思
sil_vtable funcExampleClass {
  #funcExampleClass.test1: (funcExampleClass) -> () -> () : @main.funcExampleClass.test1() -> ()    // funcExampleClass.test1()
  #funcExampleClass.test2: (funcExampleClass) -> () -> () : @main.funcExampleClass.test2() -> ()    // funcExampleClass.test2()
  #funcExampleClass.test3: (funcExampleClass) -> () -> () : @main.funcExampleClass.test3() -> ()    // funcExampleClass.test3()
  #funcExampleClass.init!allocator: (funcExampleClass.Type) -> () -> funcExampleClass : @main.funcExampleClass.__allocating_init() -> main.funcExampleClass // funcExampleClass.__allocating_init()
  #funcExampleClass.deinit!deallocator: @main.funcExampleClass.__deallocating_deinit    // funcExampleClass.__deallocating_deinit
}

sil_vtable即我们可见的vtable表,很明显看到的和前面定义的结构是一样的,'sil_vtable' identifier '{' sil-vtable-entry* '}' 其中 identifier即funcExampleClass ,{ 存储的方法,依据sil-vtable-entry定义的结构 }

我们来看一下上面调用方法的例子 example.test1()在SIL中的表现

  %8 = load %3 : $*funcExampleClass               // users: %9, %10
  %9 = class_method %8 : $funcExampleClass, #funcExampleClass.test1 : (funcExampleClass) -> () -> (), $@convention(method) (@guaranteed funcExampleClass) -> () // user: %10
  %10 = apply %9(%8) : $@convention(method) (@guaranteed funcExampleClass) -> ()

%10 的作用是调用%9中的方法,而%9是根据#funcExampleClass.test1 : (funcExampleClass) -> () -> ()这个方法描述取得vtable中对应的方法实现地址。这就是一个动态方法的调用过程。


方法前的关键字,除了访问权限控制用的 private fileprivate internal public 之外要特别指出的就是final这个关键字,有如下代码

class FuncClass {
    func test() {}
    final func test1() {}
}
class secondFuncClass: FuncClass {
    final override func test() {}
}
class thirdFuncClass: secondFuncClass {
}

首先funcClass定义的test1并不能被子类重载,secondFuncClass 中重载后的test 前加final 也不能被thirdFuncClass重载,这可以作为访问权限的那些关键字作用的补充。
其次如果打断点在test1的实例调用上看汇编代码,可以神奇的发现test1变为静态方法,或者查看SIL代码在vtable列表中也是找不到test1的,这个有效的提高方法的调用效率
最后,如果一个类确认不会被继承,那在类前面加上final 如:

final class FuncClass {
    func test() {}
    final func test1() {}
}

那么不论是test还是test1都会变为静态方法,不走动态派发的方式,这能有效的提高代码的调用效率,平常写代码的时候可以注意下这类写法,特别是写SDK的时候。

上一篇 下一篇

猜你喜欢

热点阅读