该篇主要是关于各种方法调度的差异。
前面我们研究了结构体和类的底层结构,主要是属性相关信息和引用计数。那方法存储在哪里?
首先先了解下内存的分区:
- 栈区的地址 比 堆区的地址 大。
- 栈是从高地址->低地址,向下延伸,由系统自动管理,是一片连续的内存空间。
- 堆是从低地址->高地址,向上延伸,由程序员管理,堆空间结构类似于链表,是不连续的。
- 日常开发中的溢出是指堆栈溢出,可以理解为栈区与堆区边界碰撞的情况。
- 全局区、常量区都存储在Mach-O中的__TEXT cString段。
1. 静态派发
值类型对象的函数的调用方式是静态调用
,即直接地址调用
,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用。
比如结构体函数调试如下所示:
打开demo的Mach-O可执行文件,其中的__text段,就是所谓的代码段,需要执行的汇编指令都在这里。
那直接地址调用后面是符号是哪里来的,储存在哪里?
这里引出Mach-O的
符号表Symbol Tables
和字符串表String Table
两个概念。
-
Symbol Table
:存储符号位于字符串表的位置。 -
String Table
:存放了所有的变量名和函数名,以字符串形式存储。 -
Dynamic Symbol Table
:动态库函数位于符号表的偏移信息。
也就是说符号表中并不存储字符串,字符串存储在String Table。根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名
,如下所示:
总之,流程就是通过函数地址 → 符号表偏移值 → 字符串表查找字符。可以通过命令还原符号名称: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
应该是method
在methods
中的偏移(按顺序存储的情况下,也是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流程,即动态消息转发
。