Swift中的VTable簡(jiǎn)述

在Swift中方法的調(diào)度分為靜態(tài)方法直接調(diào)用與動(dòng)態(tài)分派兩種方式

  1. 靜態(tài)方法
    靜態(tài)方法表示其為不可變的,為了提高調(diào)用的效率蘋果允許直接訪問方法地址來調(diào)用該方法,比如說結(jié)構(gòu)體中的方法
struct firstStruct {
    func test() {}
}
firstStruct().test()

斷點(diǎn)在匯編可以看到其直接調(diào)用了該方法地址


0x1040d4640=ASLR+靜態(tài)分析地址
  1. 動(dòng)態(tài)派發(fā)
    除了靜態(tài)方法外的其他方法都是通過VTable表查詢的方式進(jìn)行調(diào)用了,看一下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表中,循環(huán)寫入在內(nèi)存地址中的表現(xiàn)是連續(xù)存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)。因此可以根據(jù)地址偏移來取得對(duì)應(yīng)的存儲(chǔ)數(shù)據(jù)。
同樣的,在方法后半部分是將父類中的所有可重載方法拷貝一份存入到本類對(duì)應(yīng)的VTable中。
不難猜測(cè)Swift的方法派發(fā)效率會(huì)比OC的從父類查找要來的快,以空間換時(shí)間,提升了效率。

同樣的通過查看源代碼中VTable的查找方法可以簡(jiǎn)單做一下分析

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());
// 根據(jù)方法描述取得該方法在vtable中的地址偏移
  auto vtableOffset = vtable->getVTableOffset(description) + index;
// 本身類的起始地址
  auto *words = reinterpret_cast<void * const *>(metadata);
// 得到動(dòng)態(tài)方法的當(dāng)前實(shí)際地址
  return *(words + vtableOffset);
}

調(diào)用方法是通過方法描述從VTable表中取得對(duì)應(yīng)的偏移量,然后和本類的地址起始位置相加,就是得到該派發(fā)方法的實(shí)際地址。這個(gè)和取屬性是類似的,都是實(shí)例起始地址 + 偏移量。

頂層的源代碼調(diào)用已經(jīng)很清晰的顯示了VTable的創(chuàng)建與使用過程。通過SIL中間層代碼來加強(qiáng)一下印象
先看下SIL對(duì)VTable的定義

// 其中單引號(hào)中是固定寫法,sil_vtable someClassName { class中所有方法 }
decl ::= sil-vtable
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
// 每個(gè)方法存在vtable表中的內(nèi)容都是    方法描述 : 方法名
sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name

這段的含義就是申明了sil-vtable的結(jié)構(gòu),對(duì)照示例來看:

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

對(duì)應(yīng)的SIL源碼,上述可知每個(gè)類都會(huì)調(diào)動(dòng)initClassVTable方法來得到vtable表

class funcExampleClass {
  func test1()
  func test2()
  func test3()
  @objc deinit
  init()
}
......
// 此處"#"號(hào)并不是注釋的意思
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表,很明顯看到的和前面定義的結(jié)構(gòu)是一樣的,'sil_vtable' identifier '{' sil-vtable-entry* '}' 其中 identifier即funcExampleClass ,{ 存儲(chǔ)的方法,依據(jù)sil-vtable-entry定義的結(jié)構(gòu) }

我們來看一下上面調(diào)用方法的例子 example.test1()在SIL中的表現(xiàn)

  %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 的作用是調(diào)用%9中的方法,而%9是根據(jù)#funcExampleClass.test1 : (funcExampleClass) -> () -> ()這個(gè)方法描述取得vtable中對(duì)應(yīng)的方法實(shí)現(xiàn)地址。這就是一個(gè)動(dòng)態(tài)方法的調(diào)用過程。


方法前的關(guān)鍵字,除了訪問權(quán)限控制用的 private fileprivate internal public 之外要特別指出的就是final這個(gè)關(guān)鍵字,有如下代碼

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

首先funcClass定義的test1并不能被子類重載,secondFuncClass 中重載后的test 前加final 也不能被thirdFuncClass重載,這可以作為訪問權(quán)限的那些關(guān)鍵字作用的補(bǔ)充。
其次如果打斷點(diǎn)在test1的實(shí)例調(diào)用上看匯編代碼,可以神奇的發(fā)現(xiàn)test1變?yōu)殪o態(tài)方法,或者查看SIL代碼在vtable列表中也是找不到test1的,這個(gè)有效的提高方法的調(diào)用效率
最后,如果一個(gè)類確認(rèn)不會(huì)被繼承,那在類前面加上final 如:

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

那么不論是test還是test1都會(huì)變?yōu)?strong>靜態(tài)方法,不走動(dòng)態(tài)派發(fā)的方式,這能有效的提高代碼的調(diào)用效率,平常寫代碼的時(shí)候可以注意下這類寫法,特別是寫SDK的時(shí)候。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容