Swift的 方法調(diào)度

該篇主要是關(guān)于各種方法調(diào)度的差異。

前面我們研究了結(jié)構(gòu)體和類(lèi)的底層結(jié)構(gòu),主要是屬性相關(guān)信息和引用計(jì)數(shù)。那方法存儲(chǔ)在哪里?
首先先了解下內(nèi)存的分區(qū):

內(nèi)存區(qū)域.png
  • 棧區(qū)的地址 比 堆區(qū)的地址 大。
  • 棧是從高地址->低地址,向下延伸,由系統(tǒng)自動(dòng)管理,是一片連續(xù)的內(nèi)存空間。
  • 堆是從低地址->高地址,向上延伸,由程序員管理,堆空間結(jié)構(gòu)類(lèi)似于鏈表,是不連續(xù)的。
  • 日常開(kāi)發(fā)中的溢出是指堆棧溢出,可以理解為棧區(qū)與堆區(qū)邊界碰撞的情況。
  • 全局區(qū)、常量區(qū)都存儲(chǔ)在Mach-O中的__TEXT cString段。

1. 靜態(tài)派發(fā)

值類(lèi)型對(duì)象的函數(shù)的調(diào)用方式是靜態(tài)調(diào)用,即直接地址調(diào)用,調(diào)用函數(shù)指針,這個(gè)函數(shù)指針在編譯、鏈接完成后就已經(jīng)確定了,存放在代碼段,而結(jié)構(gòu)體內(nèi)部并不存放方法。因此可以直接通過(guò)地址直接調(diào)用。
比如結(jié)構(gòu)體函數(shù)調(diào)試如下所示:

結(jié)構(gòu)體函數(shù)調(diào)試.png

打開(kāi)demo的Mach-O可執(zhí)行文件,其中的__text段,就是所謂的代碼段,需要執(zhí)行的匯編指令都在這里。

Mach-O代碼段.png

那直接地址調(diào)用后面是符號(hào)是哪里來(lái)的,儲(chǔ)存在哪里?
這里引出Mach-O的符號(hào)表Symbol Tables字符串表String Table兩個(gè)概念。

  • Symbol Table:存儲(chǔ)符號(hào)位于字符串表的位置。
  • String Table:存放了所有的變量名和函數(shù)名,以字符串形式存儲(chǔ)。
  • Dynamic Symbol Table:動(dòng)態(tài)庫(kù)函數(shù)位于符號(hào)表的偏移信息。

也就是說(shuō)符號(hào)表中并不存儲(chǔ)字符串,字符串存儲(chǔ)在String Table。根據(jù)符號(hào)表中的偏移值到字符串中查找對(duì)應(yīng)的字符,然后進(jìn)行命名重整:工程名+類(lèi)名+函數(shù)名,如下所示:

Mach-O符號(hào)表&字符串表.png

總之,流程就是通過(guò)函數(shù)地址 → 符號(hào)表偏移值 → 字符串表查找字符??梢酝ㄟ^(guò)命令還原符號(hào)名稱(chēng):xcrun swift-demangle 符號(hào)。

2. 動(dòng)態(tài)派發(fā)

class的調(diào)度方式是動(dòng)態(tài)派發(fā),顧名思義函數(shù)指針是動(dòng)態(tài)的,在調(diào)用的時(shí)候動(dòng)態(tài)查找,動(dòng)態(tài)去派發(fā)的。
這里引出V-Table(虛函數(shù)表)的概念。函數(shù)表可以理解為數(shù)組,聲明在 class內(nèi)部的方法在不加任何關(guān)鍵字修飾的過(guò)程中,是連續(xù)存放在我們當(dāng)前的地址空間中的。每個(gè)類(lèi)的 V-Table 在編譯時(shí)就會(huì)被構(gòu)建,所以與靜態(tài)派發(fā)相比多出了兩個(gè)讀取的工作:1.讀取該類(lèi)的 vtable。2.讀取函數(shù)的指針。

為什么要?jiǎng)?chuàng)建一個(gè)函數(shù)表去存儲(chǔ)方法呢?

ClassMetadata有一個(gè)TargetClassDescriptor。

struct TargetClassDescriptor {
    // 存儲(chǔ)在任何上下文描述符的第一個(gè)公共標(biāo)記
    var Flags: ContextDescriptorFlags

    // 復(fù)用的RelativeDirectPointer這個(gè)類(lèi)型,其實(shí)并不是,但看下來(lái)原理一樣
    // 父級(jí)上下文,如果是頂級(jí)上下文則為null。
    var Parent: RelativeDirectPointer<InProcess>

    // 獲取類(lèi)的名稱(chēng)
    var Name: RelativeDirectPointer<CChar>

    // 這里的函數(shù)類(lèi)型是一個(gè)替身,需要調(diào)用getAccessFunction()拿到真正的函數(shù)指針(這里沒(méi)有封裝),會(huì)得到一個(gè)MetadataAccessFunction元數(shù)據(jù)訪問(wèn)函數(shù)的指針的包裝器類(lèi),該函數(shù)提供operator()重載以使用正確的調(diào)用約定來(lái)調(diào)用它(可變長(zhǎng)參數(shù)),意外發(fā)現(xiàn)命名重整會(huì)調(diào)用這邊的方法(目前不太了解這塊內(nèi)容)。
    var AccessFunctionPtr: RelativeDirectPointer<UnsafeRawPointer>

    // 一個(gè)指向類(lèi)型的字段描述符的指針(如果有的話)。類(lèi)型字段的描述,可以從里面獲取結(jié)構(gòu)體的屬性。
    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>
    
    // 下面兩個(gè)屬性在源碼中是union類(lèi)型,所以取size大的類(lèi)型作為屬性(這里貌似一樣),具體還得判斷是否have a resilient superclass
    
    // 有resilient superclass,用ResilientMetadataBounds,表示對(duì)保存元數(shù)據(jù)擴(kuò)展的緩存的引用
    var ResilientMetadataBounds: RelativeDirectPointer<TargetStoredClassMetadataBounds>
    // 沒(méi)有resilient superclass使用MetadataNegativeSizeInWords,表示該類(lèi)元數(shù)據(jù)對(duì)象的負(fù)大小(用字節(jié)表示)
    var MetadataNegativeSizeInWords: UInt32 {
        get {
            return UInt32(ResilientMetadataBounds.offset)
        }
    }

    // 有resilient superclass,用ExtraClassFlags,表示一個(gè)Objective-C彈性類(lèi)存根的存在
    var ExtraClassFlags: ExtraClassDescriptorFlags
    // 沒(méi)有resilient superclass使用MetadataPositiveSizeInWords,表示該類(lèi)元數(shù)據(jù)對(duì)象的正大小(用字節(jié)表示)
    var MetadataPositiveSizeInWords: UInt32 {
        get {
            return ExtraClassFlags.Bits
        }
    }
    
    /**
     此類(lèi)添加到類(lèi)元數(shù)據(jù)的其他成員的數(shù)目。默認(rèn)情況下,這些數(shù)據(jù)對(duì)運(yùn)行時(shí)是不透明的,而不是在其他成員中公開(kāi);它實(shí)際上只是NumImmediateMembers * sizeof(void*)字節(jié)的數(shù)據(jù)。
     這些字節(jié)是添加在地址點(diǎn)之前還是之后,取決于areImmediateMembersNegative()方法。
     */
    var NumImmediateMembers: UInt32
    
    // 屬性個(gè)數(shù),不包含父類(lèi)的
    var NumFields: Int32
    // 存儲(chǔ)這個(gè)結(jié)構(gòu)的字段偏移向量的偏移量(記錄你屬性起始位置的開(kāi)始的一個(gè)相對(duì)于metadata的偏移量,具體看metadata的getFieldOffsets方法),如果為0,說(shuō)明你沒(méi)有屬性
    // 如果這個(gè)類(lèi)含有一個(gè)彈性的父類(lèi),那么從他的彈性父類(lèi)的metaData開(kāi)始偏移
    var FieldOffsetVectorOffset: Int32
}

TargetClassDescriptor除了擁有一些我們常用的屬性外,還可以獲取一些對(duì)象。

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

這些所有的類(lèi)對(duì)象都是緊挨在一起的。當(dāng)然這些對(duì)象的個(gè)數(shù)是不固定的,有些是0,說(shuō)明沒(méi)有,有些是1,也有些是幾個(gè),需要某處內(nèi)存處獲取個(gè)數(shù)。比如TargetMethodDescriptor,每一個(gè)Descriptor對(duì)應(yīng)一個(gè)方法。所以你要獲取其中一個(gè)類(lèi)對(duì)象的內(nèi)存地址,你必須判斷該類(lèi)對(duì)象是否存在,并且需要知道前一項(xiàng)類(lèi)對(duì)象的內(nèi)存地址。

這里常用到的VTableDescriptor和MethodDescriptor。顧名思義,一個(gè)用于存儲(chǔ)V-Table的信息,一個(gè)用于存儲(chǔ)方法的信息。

// 類(lèi)vtable描述符的頭文件。這是一個(gè)可變大小的結(jié)構(gòu),用于描述如何在類(lèi)的類(lèi)型元數(shù)據(jù)中查找和解析虛函數(shù)表。
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.
    // 用來(lái)標(biāo)示方法類(lèi)型(init getter setter等)
    var Flags: MethodDescriptorFlags
    // The method implementation.
    // 方法的相對(duì)指針
    var Impl: RelativeDirectPointer<UnsafeMutableRawPointer>
}

另外還有OverrideTableDescriptor和MethodOverrideDescriptor。這兩個(gè)就是分別存儲(chǔ)重寫(xiě)方法的個(gè)數(shù)和重寫(xiě)方法的描述信息。

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是如何創(chuàng)建的:

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

  if (description->hasVTable()) {
    //  獲取vtable的相關(guān)信息
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    // 獲取方法描述集合
    auto descriptors = description->getMethodDescriptors();
    // &classWords[vtableOffset]可以看成是V-Table的首地址
    // 將方法描述中的方法指針按順序存儲(chǔ)在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.
      // 指向基類(lèi)的地址
      auto *baseClass = cast_or_null<ClassDescriptor>(descriptor.Class.get());
      //  指向原來(lái)(基類(lèi))的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.
      // 基類(lèi)的MethodDescriptors
      auto baseClassMethods = baseClass->getMethodDescriptors();

      // If the method descriptor doesn't land within the bounds of the
      // method table, abort.
      // 如果baseMethod不符合在基類(lèi)的MethodDescriptors中間,報(bào)錯(cuò)
      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();
       // 基類(lèi)的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());
    }
  }
}

創(chuàng)建方法主要分成兩部分:
① 獲取vtable信息,獲取方法descriptions,將方法Description的指針Imp(未重寫(xiě)的)存儲(chǔ)在V-Table(元數(shù)據(jù)地址 + vtableOffset )中。
②獲取OverrideTable信息,獲取overrideDescriptors,將description的指針Imp(重寫(xiě)的)存儲(chǔ)在V-Table(offset )中,此處的offset為基類(lèi)的vTable地址 +baseMethod在baseClassMethods的index???。

可以知道的是一個(gè)類(lèi)的V-Table是由自身方法和重寫(xiě)方法組成,對(duì)比OC重寫(xiě)方法需要去父類(lèi)去查找,Swift用空間換時(shí)間,提高了查找效率。
另外,我們?cè)賮?lái)看查找方法:

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;
}

簡(jiǎn)單說(shuō),就是通過(guò)方法在V-Table中的偏移,獲取對(duì)應(yīng)的方法指針,然后跳轉(zhuǎn)執(zhí)行。
此處的index應(yīng)該是methodmethods中的偏移(按順序存儲(chǔ)的情況下,也是method在V-Table中的偏移)。所以方法指針相對(duì)于原數(shù)據(jù)的偏移就是vtableOffset+index。

為什么要?jiǎng)?chuàng)建V-Table來(lái)進(jìn)行方法調(diào)用呢?
我的理解是,提高調(diào)用效率,在不將方法指針存儲(chǔ)在V-Table的情況下,方法查找起碼需要ClassMetadata → Description → MethodDescription → Imp這么些步驟,更何況查找MethodDescription的步驟又是需要先查找其他對(duì)象等復(fù)雜的步驟。所以將方法指針提取出來(lái),放在數(shù)組是效率最高的。

3.總結(jié)

以下是關(guān)于一些關(guān)鍵字的函數(shù)調(diào)用形式的結(jié)論(暫未調(diào)試):

① struct是值類(lèi)型,其中函數(shù)的調(diào)度屬于直接調(diào)用地址,即靜態(tài)調(diào)度。
② class是引用類(lèi)型,其中函數(shù)的調(diào)度是通過(guò)V-Table函數(shù)表來(lái)進(jìn)行調(diào)度的,即動(dòng)態(tài)調(diào)度
③ extension中的函數(shù)調(diào)度方式是直接調(diào)度。
final修飾的函數(shù)調(diào)度方式是直接調(diào)度。
④ @objc修飾的函數(shù)調(diào)度方式是函數(shù)表調(diào)度,如果OC中需要使用,class還必須繼承NSObject。
⑤ dynamic修飾的函數(shù)的調(diào)度方式是函數(shù)表調(diào)度,使函數(shù)具有動(dòng)態(tài)性。
⑥ @objc + dynamic 組合修飾的函數(shù)調(diào)度,是執(zhí)行的是objc_msgSend流程,即動(dòng)態(tài)消息轉(zhuǎn)發(fā)。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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