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

- 棧區(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)試如下所示:

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

那直接地址調(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ù)名,如下所示:

總之,流程就是通過(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)該是method在methods中的偏移(按順序存儲(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ā)。