Swift的 方法調(diào)度

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

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

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

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

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

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

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

Mach-O代碼段.png

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

  • Symbol Table:存儲符號位于字符串表的位置。
  • String Table:存放了所有的變量名和函數(shù)名蒋情,以字符串形式存儲埠况。
  • Dynamic Symbol Table:動態(tài)庫函數(shù)位于符號表的偏移信息。

也就是說符號表中并不存儲字符串棵癣,字符串存儲在String Table辕翰。根據(jù)符號表中的偏移值到字符串中查找對應(yīng)的字符,然后進(jìn)行命名重整:工程名+類名+函數(shù)名狈谊,如下所示:

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

總之喜命,流程就是通過函數(shù)地址 → 符號表偏移值 → 字符串表查找字符『尤埃可以通過命令還原符號名稱:xcrun swift-demangle 符號壁榕。

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

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

為什么要創(chuàng)建一個(gè)函數(shù)表去存儲方法呢因妙?

ClassMetadata有一個(gè)TargetClassDescriptor痰憎。

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

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

    // 獲取類的名稱
    var Name: RelativeDirectPointer<CChar>

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

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

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

TargetClassDescriptor除了擁有一些我們常用的屬性外雅潭,還可以獲取一些對象揭厚。

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

這些所有的類對象都是緊挨在一起的。當(dāng)然這些對象的個(gè)數(shù)是不固定的扶供,有些是0筛圆,說明沒有,有些是1椿浓,也有些是幾個(gè)太援,需要某處內(nèi)存處獲取個(gè)數(shù)。比如TargetMethodDescriptor扳碍,每一個(gè)Descriptor對應(yīng)一個(gè)方法提岔。所以你要獲取其中一個(gè)類對象的內(nèi)存地址,你必須判斷該類對象是否存在笋敞,并且需要知道前一項(xiàng)類對象的內(nèi)存地址碱蒙。

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

// 類vtable描述符的頭文件鞭莽。這是一個(gè)可變大小的結(jié)構(gòu)坊秸,用于描述如何在類的類型元數(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.
    // 用來標(biāo)示方法類型(init getter setter等)
    var Flags: MethodDescriptorFlags
    // The method implementation.
    // 方法的相對指針
    var Impl: RelativeDirectPointer<UnsafeMutableRawPointer>
}

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

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的首地址
    // 將方法描述中的方法指針按順序存儲在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中間,報(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();
       // 基類的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(未重寫的)存儲在V-Table(元數(shù)據(jù)地址 + vtableOffset )中。
②獲取OverrideTable信息盒齿,獲取overrideDescriptors念逞,將description的指針Imp(重寫的)存儲在V-Table(offset )中,此處的offset為基類的vTable地址 +baseMethod在baseClassMethods的index边翁?翎承??符匾。

可以知道的是一個(gè)類的V-Table是由自身方法和重寫方法組成叨咖,對比OC重寫方法需要去父類去查找,Swift用空間換時(shí)間,提高了查找效率甸各。
另外垛贤,我們再來看查找方法:

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中的偏移趣倾,獲取對應(yīng)的方法指針聘惦,然后跳轉(zhuǎn)執(zhí)行。
此處的index應(yīng)該是methodmethods中的偏移(按順序存儲的情況下誊酌,也是method在V-Table中的偏移)部凑。所以方法指針相對于原數(shù)據(jù)的偏移就是vtableOffset+index

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

3.總結(jié)

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

① struct是值類型,其中函數(shù)的調(diào)度屬于直接調(diào)用地址臊恋,即靜態(tài)調(diào)度衣洁。
② class是引用類型,其中函數(shù)的調(diào)度是通過V-Table函數(shù)表來進(jìn)行調(diào)度的抖仅,即動態(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ù)具有動態(tài)性。
⑥ @objc + dynamic 組合修飾的函數(shù)調(diào)度渡紫,是執(zhí)行的是objc_msgSend流程到推,即動態(tài)消息轉(zhuǎn)發(fā)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惕澎,一起剝皮案震驚了整個(gè)濱河市莉测,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌集灌,老刑警劉巖悔雹,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異欣喧,居然都是意外死亡腌零,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門唆阿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來益涧,“玉大人,你說我怎么就攤上這事驯鳖∠醒” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵浅辙,是天一觀的道長扭弧。 經(jīng)常有香客問我,道長记舆,這世上最難降的妖魔是什么鸽捻? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮泽腮,結(jié)果婚禮上御蒲,老公的妹妹穿的比我還像新娘。我一直安慰自己诊赊,他們只是感情好厚满,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碧磅,像睡著了一般碘箍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上续崖,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天敲街,我揣著相機(jī)與錄音,去河邊找鬼严望。 笑死多艇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的像吻。 我是一名探鬼主播峻黍,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拨匆!你這毒婦竟也來了姆涩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤惭每,失蹤者是張志新(化名)和其女友劉穎骨饿,沒想到半個(gè)月后亏栈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宏赘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年绒北,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片察署。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闷游,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贴汪,到底是詐尸還是另有隱情脐往,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布扳埂,位于F島的核電站业簿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阳懂。R本人自食惡果不足惜辖源,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望希太。 院中可真熱鬧克饶,春花似錦、人聲如沸誊辉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽堕澄。三九已至邀跃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蛙紫,已是汗流浹背拍屑。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坑傅,地道東北人僵驰。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像唁毒,于是被迫代替她去往敵國和親蒜茴。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容