Swift的 方法调度

该篇主要是关于各种方法调度的差异。

前面我们研究了结构体和类的底层结构,主要是属性相关信息和引用计数。那方法存储在哪里?
首先先了解下内存的分区:

内存区域.png
  • 栈区的地址 比 堆区的地址 大。
  • 栈是从高地址->低地址,向下延伸,由系统自动管理,是一片连续的内存空间。
  • 堆是从低地址->高地址,向上延伸,由程序员管理,堆空间结构类似于链表,是不连续的。
  • 日常开发中的溢出是指堆栈溢出,可以理解为栈区与堆区边界碰撞的情况。
  • 全局区、常量区都存储在Mach-O中的__TEXT cString段。

1. 静态派发

值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用。
比如结构体函数调试如下所示:

结构体函数调试.png

打开demo的Mach-O可执行文件,其中的__text段,就是所谓的代码段,需要执行的汇编指令都在这里。

Mach-O代码段.png

那直接地址调用后面是符号是哪里来的,储存在哪里?
这里引出Mach-O的符号表Symbol Tables字符串表String Table两个概念。

  • Symbol Table:存储符号位于字符串表的位置。
  • String Table:存放了所有的变量名和函数名,以字符串形式存储。
  • Dynamic Symbol Table:动态库函数位于符号表的偏移信息。

也就是说符号表中并不存储字符串,字符串存储在String Table。根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名,如下所示:

Mach-O符号表&字符串表.png

总之,流程就是通过函数地址 → 符号表偏移值 → 字符串表查找字符。可以通过命令还原符号名称:xcrun swift-demangle 符号。

2. 动态派发

class的调度方式是动态派发,顾名思义函数指针是动态的,在调用的时候动态查找,动态去派发的。
这里引出V-Table(虚函数表)的概念。函数表可以理解为数组,声明在 class内部的方法在不加任何关键字修饰的过程中,是连续存放在我们当前的地址空间中的。每个类的 V-Table 在编译时就会被构建,所以与静态派发相比多出了两个读取的工作:1.读取该类的 vtable。2.读取函数的指针。

为什么要创建一个函数表去存储方法呢?

ClassMetadata有一个TargetClassDescriptor。

struct TargetClassDescriptor {
    // 存储在任何上下文描述符的第一个公共标记
    var Flags: ContextDescriptorFlags

    // 复用的RelativeDirectPointer这个类型,其实并不是,但看下来原理一样
    // 父级上下文,如果是顶级上下文则为null。
    var Parent: RelativeDirectPointer<InProcess>

    // 获取类的名称
    var Name: RelativeDirectPointer<CChar>

    // 这里的函数类型是一个替身,需要调用getAccessFunction()拿到真正的函数指针(这里没有封装),会得到一个MetadataAccessFunction元数据访问函数的指针的包装器类,该函数提供operator()重载以使用正确的调用约定来调用它(可变长参数),意外发现命名重整会调用这边的方法(目前不太了解这块内容)。
    var AccessFunctionPtr: RelativeDirectPointer<UnsafeRawPointer>

    // 一个指向类型的字段描述符的指针(如果有的话)。类型字段的描述,可以从里面获取结构体的属性。
    var Fields: RelativeDirectPointer<FieldDescriptor>
    
    // The type of the superclass, expressed as a mangled type name that can refer to the generic arguments of the subclass type.
    var SuperclassType: RelativeDirectPointer<CChar>
    
    // 下面两个属性在源码中是union类型,所以取size大的类型作为属性(这里貌似一样),具体还得判断是否have a resilient superclass
    
    // 有resilient superclass,用ResilientMetadataBounds,表示对保存元数据扩展的缓存的引用
    var ResilientMetadataBounds: RelativeDirectPointer<TargetStoredClassMetadataBounds>
    // 没有resilient superclass使用MetadataNegativeSizeInWords,表示该类元数据对象的负大小(用字节表示)
    var MetadataNegativeSizeInWords: UInt32 {
        get {
            return UInt32(ResilientMetadataBounds.offset)
        }
    }

    // 有resilient superclass,用ExtraClassFlags,表示一个Objective-C弹性类存根的存在
    var ExtraClassFlags: ExtraClassDescriptorFlags
    // 没有resilient superclass使用MetadataPositiveSizeInWords,表示该类元数据对象的正大小(用字节表示)
    var MetadataPositiveSizeInWords: UInt32 {
        get {
            return ExtraClassFlags.Bits
        }
    }
    
    /**
     此类添加到类元数据的其他成员的数目。默认情况下,这些数据对运行时是不透明的,而不是在其他成员中公开;它实际上只是NumImmediateMembers * sizeof(void*)字节的数据。
     这些字节是添加在地址点之前还是之后,取决于areImmediateMembersNegative()方法。
     */
    var NumImmediateMembers: UInt32
    
    // 属性个数,不包含父类的
    var NumFields: Int32
    // 存储这个结构的字段偏移向量的偏移量(记录你属性起始位置的开始的一个相对于metadata的偏移量,具体看metadata的getFieldOffsets方法),如果为0,说明你没有属性
    // 如果这个类含有一个弹性的父类,那么从他的弹性父类的metaData开始偏移
    var FieldOffsetVectorOffset: Int32
}

TargetClassDescriptor除了拥有一些我们常用的属性外,还可以获取一些对象。

  • TargetClassDescriptor
  • TargetTypeGenericContextDescriptorHeader
  • GenericParamDescriptor
  • TargetGenericRequirementDescriptor
  • TargetResilientSuperclass
  • TargetForeignMetadataInitialization
  • TargetSingletonMetadataInitialization
  • TargetVTableDescriptorHeader
  • TargetMethodDescriptor
  • TargetOverrideTableHeader
  • TargetMethodOverrideDescriptor
  • TargetObjCResilientClassStubInfo

这些所有的类对象都是紧挨在一起的。当然这些对象的个数是不固定的,有些是0,说明没有,有些是1,也有些是几个,需要某处内存处获取个数。比如TargetMethodDescriptor,每一个Descriptor对应一个方法。所以你要获取其中一个类对象的内存地址,你必须判断该类对象是否存在,并且需要知道前一项类对象的内存地址。

这里常用到的VTableDescriptor和MethodDescriptor。顾名思义,一个用于存储V-Table的信息,一个用于存储方法的信息。

// 类vtable描述符的头文件。这是一个可变大小的结构,用于描述如何在类的类型元数据中查找和解析虚函数表。
struct TargetVTableDescriptorHeader {
    var VTableOffset: UInt32
    var VTableSize: UInt32
    func getVTableOffset(description: UnsafeMutablePointer<TargetClassDescriptor>) -> UInt32 {
        if description.pointee.hasResilientSuperclass() {
            let bounds = description.pointee.getMetadataBounds()
            return UInt32(bounds.ImmediateMembersOffset / MemoryLayout<UnsafeRawPointer>.size) + VTableOffset
        }
        return VTableOffset
    }
}
struct TargetMethodDescriptor {
    // Flags describing the method.
    // 用来标示方法类型(init getter setter等)
    var Flags: MethodDescriptorFlags
    // The method implementation.
    // 方法的相对指针
    var Impl: RelativeDirectPointer<UnsafeMutableRawPointer>
}

另外还有OverrideTableDescriptor和MethodOverrideDescriptor。这两个就是分别存储重写方法的个数和重写方法的描述信息。

struct TargetOverrideTableHeader {
    // The number of MethodOverrideDescriptor records following the vtable override header in the class's nominal type descriptor.
    var NumEntries: UInt32
};

struct TargetMethodOverrideDescriptor {
    // The class containing the base method.
    var Class: RelativeIndirectablePointer<UnsafeMutableRawPointer>
    // The base method.
    var Method: RelativeIndirectablePointer<UnsafeMutableRawPointer>
    // The implementation of the override.
    var Impl: RelativeDirectPointer<UnsafeMutableRawPointer>
}

首先我们看V-Table是如何创建的:

static void initClassVTable(ClassMetadata *self) {
  const auto *description = self->getDescription();
  // 可以看成是Metadata地址
  auto *classWords = reinterpret_cast<void **>(self);

  if (description->hasVTable()) {
    //  获取vtable的相关信息
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    // 获取方法描述集合
    auto descriptors = description->getMethodDescriptors();
    // &classWords[vtableOffset]可以看成是V-Table的首地址
    // 将方法描述中的方法指针按顺序存储在V-Table中
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
      auto &methodDescription = descriptors[i];
      swift_ptrauth_init_code_or_data(
          &classWords[vtableOffset + i], methodDescription.getImpl(),
          methodDescription.Flags.getExtraDiscriminator(),
          !methodDescription.Flags.isAsync());
    }
  }

  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());
      //  指向原来(基类)的MethodDescriptor地址
      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.
      // 基类的MethodDescriptors
      auto baseClassMethods = baseClass->getMethodDescriptors();

      // If the method descriptor doesn't land within the bounds of the
      // method table, abort.
      // 如果baseMethod不符合在基类的MethodDescriptors中间,报错
      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();
       // 基类的vTable地址 + baseMethod在baseClassMethods的index???
      auto offset = (baseVTable- >getVTableOffset(baseClass) +
                     (baseMethod - baseClassMethods.data()));
      swift_ptrauth_init_code_or_data(&classWords[offset],
                                      descriptor.getImpl(),
                                      baseMethod->Flags.getExtraDiscriminator(),
                                      !baseMethod->Flags.isAsync());
    }
  }
}

创建方法主要分成两部分:
① 获取vtable信息,获取方法descriptions,将方法Description的指针Imp(未重写的)存储在V-Table(元数据地址 + vtableOffset )中。
②获取OverrideTable信息,获取overrideDescriptors,将description的指针Imp(重写的)存储在V-Table(offset )中,此处的offset为基类的vTable地址 +baseMethod在baseClassMethods的index???。

可以知道的是一个类的V-Table是由自身方法和重写方法组成,对比OC重写方法需要去父类去查找,Swift用空间换时间,提高了查找效率。
另外,我们再来看查找方法:

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

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

  auto methods = description->getMethodDescriptors();
  unsigned index = method - methods.data();
  assert(index < methods.size());

  auto vtableOffset = vtable->getVTableOffset(description) + index;
  auto *words = reinterpret_cast<void * const *>(metadata);

  auto *const *methodPtr = (words + vtableOffset);

  return *methodPtr;
}

简单说,就是通过方法在V-Table中的偏移,获取对应的方法指针,然后跳转执行。
此处的index应该是methodmethods中的偏移(按顺序存储的情况下,也是method在V-Table中的偏移)。所以方法指针相对于原数据的偏移就是vtableOffset+index

为什么要创建V-Table来进行方法调用呢?
我的理解是,提高调用效率,在不将方法指针存储在V-Table的情况下,方法查找起码需要ClassMetadata → Description → MethodDescription → Imp这么些步骤,更何况查找MethodDescription的步骤又是需要先查找其他对象等复杂的步骤。所以将方法指针提取出来,放在数组是效率最高的。

3.总结

以下是关于一些关键字的函数调用形式的结论(暂未调试):

① struct是值类型,其中函数的调度属于直接调用地址,即静态调度
② class是引用类型,其中函数的调度是通过V-Table函数表来进行调度的,即动态调度
③ extension中的函数调度方式是直接调度
final修饰的函数调度方式是直接调度
④ @objc修饰的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject。
⑤ dynamic修饰的函数的调度方式是函数表调度,使函数具有动态性。
⑥ @objc + dynamic 组合修饰的函数调度,是执行的是objc_msgSend流程,即动态消息转发

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

推荐阅读更多精彩内容