Swift(六)-方法调度(上)

3,075 阅读4分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战

我们都知道,在OC中方法的调用是通过objc_msgSend来发送消息的;那么在Swift中,方法的调用时如何实现的呢?

方法的查找

我们来看如下代码:

image.png

ViewController类中,我们新建了一个Teacher类,在其中定义了一个teach方法,在viewDidLoad方法中初始化Teacher并调用teach方法(为了避免其他调用产生的代码,我们将super.viewDidLoad的调用删除掉),我们在调用teach方法的时候打上断点,此时,我们查看汇编页面的情况:

image.png

在汇编指令中,我们分析方法的调用主要是通过blblr两个指令;我们留意到在调用__allocating_initswift_release两个bl指令之间还有一个blr指令,那么这个blr执行是否就是调用的teach方法呢?我们向下执行汇编指令到blr x8

image.png

通过指令打印,我们确定此时就是在调用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地址偏移一定的大小,就能够找到实例对象的方法;

函数表

那么如果有多个方法呢?我们再添加两个方法teach1teach2个方法,然后也调用这两个方法,来看一下汇编代码:

image.png

我们可以看到三个方法的调用,分别对应了三个blr x8,而且这三个方法的地址相差8字节,也就是一个函数指针的大小;在内存中,这三个函数的内存地址是连续的;那么Swift中函数的调度就是基于函数表的调度;

接下来,我们通过SIL文件来验证一下:

image.png

通过生成的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属性,不管是ClassStruct还是Enum都有自己的Descriptor,这是对类的详细描述;通过Swift源码可以看到其类型为TargetClassDescriptor

image.png

通过分析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发现该类有一个别名:

image.png

在源码中我们搜索ClassDescriptor,我们在源码中全文搜索发现内容太多,那么我们怎么找呢?我们通过文件名称发现有一个GenMeta.cpp的文件,通过名称可以大胆猜测该文件大概率是生成Metadata的,通过此处GenMeta.cpp文件的搜索结果:

image.png

我们可以定位到类ClassContextDescriptorBuilder这是当前类的描述的创建者;在该类的layout方法中我们可以看到如下的实现:

image.png

首先我们查看super::layout的实现:

image.png

可以看到,此处是在创建我们之前分析的TargetClassDescriptor的结构体,并给属性赋值; 接下来,我们查看addVTable方法的实现:

image.png

我们大胆猜测,此处的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文件来验证上述结论;