Swift中的VTable简述

在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的时候。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,699评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,124评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,127评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,342评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,356评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,057评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,654评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,572评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,205评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,343评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,015评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,704评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,196评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,690评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,348评论 2 358

推荐阅读更多精彩内容