「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」
我们都知道,在OC中方法的调用是通过objc_msgSend来发送消息的;那么在Swift中,方法的调用时如何实现的呢?
方法的查找
我们来看如下代码:
在ViewController类中,我们新建了一个Teacher类,在其中定义了一个teach方法,在viewDidLoad方法中初始化Teacher并调用teach方法(为了避免其他调用产生的代码,我们将super.viewDidLoad的调用删除掉),我们在调用teach方法的时候打上断点,此时,我们查看汇编页面的情况:
在汇编指令中,我们分析方法的调用主要是通过bl和blr两个指令;我们留意到在调用__allocating_init和swift_release两个bl指令之间还有一个blr指令,那么这个blr执行是否就是调用的teach方法呢?我们向下执行汇编指令到blr x8:
通过指令打印,我们确定此时就是在调用teach方法,那么teach方法是如何找到的呢?我们来逐行分析汇编代码:
bl 0x104012a1c:此行汇编指令调用了__allocating_init方法,返回了一个Teacher的实例对象,返回值是放在了x0寄存器中;mov x20, x0:将寄存器x0的值复制到x20寄存器,此时x20寄存器存放的就是Teacher的实例对象;str x20, [sp, #0x8]和str x20, [sp, #0x10]是将寄存器x20的值写入到内存中,我们可以不做关注;ldr x8, [x20]:将x20寄存器中的值读取到x8寄存器中,因为是64位架构,所以此处读取的是8字节,x20中存放的是Teacher实例对象,实例对象的第一个8字节是metadata,此时的x8寄存器存放的是实例对象的metadata;ldr x8, [x8, #0x50]:将寄存器x8中的地址偏移0x50的大小,然后将偏移后的地址存放进x8;blr x8:跳转到x8寄存器的地址,通过打印我们可以看到此时的x8就是teach方法;
teach方法的调用过程:先找到实例对象的metadata,通过metadata地址偏移一定的大小,就能够找到实例对象的方法;
函数表
那么如果有多个方法呢?我们再添加两个方法teach1和teach2个方法,然后也调用这两个方法,来看一下汇编代码:
我们可以看到三个方法的调用,分别对应了三个blr x8,而且这三个方法的地址相差8字节,也就是一个函数指针的大小;在内存中,这三个函数的内存地址是连续的;那么Swift中函数的调度就是基于函数表的调度;
接下来,我们通过SIL文件来验证一下:
通过生成的SIL可以发现,Teacher的三个方法都是存放在sil_vtable中的,他就是类的函数表;
TargetClassDescriptor
我们已经分析过Metadata的结构如下:
struct Metadata{
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
在此结构中我们需要注意这样一个typeDescriptor属性,不管是Class,Struct还是Enum都有自己的Descriptor,这是对类的详细描述;通过Swift源码可以看到其类型为TargetClassDescriptor:
通过分析TargetClassDescriptor及其继承关系,可以分析出其数据结构大致如下:
struct TargetClassDescriptor {
ContextDescriptorFlags Flags;
TargetRelativeContextPointer<Runtime> Parent;
TargetRelativeDirectPointer<Runtime, const char, /*nullable*/ false> Name;
TargetRelativeDirectPointer<Runtime, MetadataResponse(...),
/*Nullable*/ true> AccessFunctionPtr;
TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor,
/*nullable*/ true> Fields;
TargetRelativeDirectPointer<Runtime, const char> SuperclassType;
uint32_t MetadataNegativeSizeInWords;
uint32_t MetadataPositiveSizeInWords;
uint32_t NumImmediateMembers;
uint32_t NumFields;
uint32_t FieldOffsetVectorOffset;
}
在其中并没有vtable相关的属性;我们全文搜索TargetClassDescriptor发现该类有一个别名:
在源码中我们搜索ClassDescriptor,我们在源码中全文搜索发现内容太多,那么我们怎么找呢?我们通过文件名称发现有一个GenMeta.cpp的文件,通过名称可以大胆猜测该文件大概率是生成Metadata的,通过此处GenMeta.cpp文件的搜索结果:
我们可以定位到类ClassContextDescriptorBuilder这是当前类的描述的创建者;在该类的layout方法中我们可以看到如下的实现:
首先我们查看super::layout的实现:
可以看到,此处是在创建我们之前分析的TargetClassDescriptor的结构体,并给属性赋值;
接下来,我们查看addVTable方法的实现:
我们大胆猜测,此处的B就是我们的结构体TargetClassDescriptor,此结构体可不补全为:
struct TargetClassDescriptor {
ContextDescriptorFlags Flags;
TargetRelativeContextPointer<Runtime> Parent;
TargetRelativeDirectPointer<Runtime, const char, /*nullable*/ false> Name;
TargetRelativeDirectPointer<Runtime, MetadataResponse(...),
/*Nullable*/ true> AccessFunctionPtr;
TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor,
/*nullable*/ true> Fields;
TargetRelativeDirectPointer<Runtime, const char> SuperclassType;
uint32_t MetadataNegativeSizeInWords;
uint32_t MetadataPositiveSizeInWords;
uint32_t NumImmediateMembers;
uint32_t NumFields;
uint32_t FieldOffsetVectorOffset;
uint32_t offset;
uint32_t size;
// 接下来是vtable
}
那么我们如果验证上面的结论是正确的呢?下一篇文章我们通过MachO文件来验证上述结论;