該篇主要是關(guān)于各種方法調(diào)度的差異化借。
前面我們研究了結(jié)構(gòu)體和類的底層結(jié)構(gòu)潜慎,主要是屬性相關(guān)信息和引用計(jì)數(shù)。那方法存儲在哪里蓖康?
首先先了解下內(nèi)存的分區(qū):
- 棧區(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)試如下所示:
打開demo的Mach-O可執(zhí)行文件森渐,其中的__text段,就是所謂的代碼段冒晰,需要執(zhí)行的匯編指令都在這里同衣。
那直接地址調(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ù)名
狈谊,如下所示:
總之喜命,流程就是通過函數(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)該是method
在methods
中的偏移(按順序存儲的情況下誊酌,也是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ā)
。