美文网首页
Swift底层原理-方法调度

Swift底层原理-方法调度

作者: 祀梦_ | 来源:发表于2022-12-11 23:36 被阅读0次

Swift底层原理-方法调度

  • 我们知道,在OC中方法的调用是通过objc_msgSend来发送消息的;那么在Swift中,方法的调用时如何实现的呢?
  • 而且在swift中不仅仅只有类可以定义方法,结构体也可以定义方法。下面就让我们分别研究一下他们的方法调用

结构体方法

  • 我们通过汇编来查看一下,调用结构体的方法时,是如何调用的
struct Test {
    func test() {
        
    }
    
    func test1() {
        
    }
}

let test = Test()
test.test()
test.test1()
  • 打下断点,进入汇编代码:
image
  • 可以发现,在Swift中,调用一个结构体的方法是直接拿到函数的地址直接调用,包括初始化方法。
  • Swift是一门静态语言,许多东西在编译器就已经确定了,所以才可以直接拿到函数的地址进行调用,这个调用的形式也可以称作静态派发
  • 这个函数地址在编译器决定,并存储在__text段中,也就是代码段中

extension中方法调用

  • Test添加一个extension,创建一个test3方法:
extension Test {
    func test3() {
        
    }
}
  • 通过汇编查看
-
  • structextension的方法依然是直接调用(静态派发)

类方法

  • 前面我们已经了解了Swift结构体的方法调用,那么Swift的类呢
class Test {
    func test() {
        
    }
    
    func test1() {
        
    }
}

let test = Test()
test.test()
test.test1()
  • 开启汇编调试
image
  • 在看汇编代码前,可以简单的认识几个汇编指令,就可以大致了解以上汇编内容

    • mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如
    mov x1, x0    将寄存器x0的值符知道寄存器x1中
    
    • ldr:将内存中的值读取到寄存器中,如:
    ldr x0, [x1, x2] 将寄存器x1和寄存器x2的值相加作为地址,取改地址的值放入寄存器x0中
    
    • blblr:跳转到某地址(有返回)
    • x代表寄存器,x0用来存放函数计算结果
  • 在第8行,mov x20, x0x0里面存放的是test对象

  • 在第14行,ldr x8, [x20],把test对象首地址,也就是metedata放在x8中。

  • 然后通过对metedate的偏移,拿到函数的地址。

  • 总结:swift中函数调用分为了3个步骤

    1. 找到metadata
    2. 确定函数地址(metadata + 偏移量)
    3. 执行函数
  • 那么这些函数地址存放在哪里呢?

函数表

  • 我们生成sil文件,看一下编译时做了哪些操作

  • 来到sil文件底部

sil_vtable Test {
  #Test.test: (Test) -> () -> () : @$s4main4TestC4testyyF   // Test.test()
  #Test.test1: (Test) -> () -> () : @$s4main4TestC5test1yyF // Test.test1()
  #Test.init!allocator: (Test.Type) -> () -> Test : @$s4main4TestCACycfC    // Test.__allocating_init()
  #Test.deinit!deallocator: @$s4main4TestCfD    // Test.__deallocating_deinit
}
  • 通过sil可以发现,Test的三个方法都是存放在sil_vtable中的,他就是类的函数表;

  • 函数表用来存储类中的方法,存储方式类似于数组,方法连续存放在函数表中。

函数表在类中位置

  • 在上一篇文章结构体与类中,我们把Swift类的本质挖掘出来了,它里面有一个 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
  • 我们从源码中找到Description定义,发现它是TargetClassDescriptor 类型的类
template <typename Runtime>
class TargetClassDescriptor final
    : public TargetTypeContextDescriptor<Runtime>,
      public TrailingGenericContextObjects<TargetClassDescriptor<Runtime>,
                              TargetTypeGenericContextDescriptorHeader,
                              /*additional trailing objects:*/
                              TargetResilientSuperclass<Runtime>,
                              TargetForeignMetadataInitialization<Runtime>,
                              TargetSingletonMetadataInitialization<Runtime>,
                              TargetVTableDescriptorHeader<Runtime>,
                              TargetMethodDescriptor<Runtime>,
                              TargetOverrideTableHeader<Runtime>,
                              TargetMethodOverrideDescriptor<Runtime>,
                              TargetObjCResilientClassStubInfo<Runtime>> {
    // 省略具体实现
}
  • 根据继承关系慢慢对比,对比出来的结果,TargetClassDescriptor里面的属性如下
class 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相关的属性,我们想法是找到这个类的初始化方法,里面肯定有关于属性的初始化流程。然后找到ClassContextDescriptorBuilder这样一个类,内容的描述建立者,这个类就是创建 Descriptor 的类。
class ClassContextDescriptorBuilder
    : public TypeContextDescriptorBuilderBase<ClassContextDescriptorBuilder,
                                              ClassDecl>,
      public SILVTableVisitor<ClassContextDescriptorBuilder>
  {
    using super = TypeContextDescriptorBuilderBase;
  
    ClassDecl *getType() {
      return cast<ClassDecl>(Type);
    }

    // Non-null unless the type is foreign.
    ClassMetadataLayout *MetadataLayout = nullptr;

    Optional<TypeEntityReference> ResilientSuperClassRef;

    SILVTable *VTable;
    bool Resilient;

    SmallVector<SILDeclRef, 8> VTableEntries;
    SmallVector<std::pair<SILDeclRef, SILDeclRef>, 8> OverrideTableEntries;

  public:
    ClassContextDescriptorBuilder(IRGenModule &IGM, ClassDecl *Type,
                                  RequireMetadata_t requireMetadata)
      : super(IGM, Type, requireMetadata),
        VTable(IGM.getSILModule().lookUpVTable(getType())),
        Resilient(IGM.hasResilientMetadata(Type, ResilienceExpansion::Minimal)) {

      if (getType()->isForeign()) return;

      MetadataLayout = &IGM.getClassMetadataLayout(Type);

      if (auto superclassDecl = getType()->getSuperclassDecl()) {
        if (MetadataLayout && MetadataLayout->hasResilientSuperclass())
          ResilientSuperClassRef = IGM.getTypeEntityReference(superclassDecl);
      }

      addVTableEntries(getType());
    }

    void addMethod(SILDeclRef fn) {
      VTableEntries.push_back(fn);
    }

    void addMethodOverride(SILDeclRef baseRef, SILDeclRef declRef) {
      OverrideTableEntries.emplace_back(baseRef, declRef);
    }

    void layout() {
      super::layout();
      addVTable();
      addOverrideTable();
      addObjCResilientClassStubInfo();
    }
          
    // 省略部分方法
}
  • 在类中找到 layout 这个方法:
void layout() {
    super::layout();
    addVTable();
    addOverrideTable();
    addObjCResilientClassStubInfo();
}
  • 在这里调用了addVTable方法
void addVTable() {
    if (VTableEntries.empty())
        return;

    // Only emit a method lookup function if the class is resilient
    // and has a non-empty vtable.
    if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal))
        IGM.emitMethodLookupFunction(getType());

    auto offset = MetadataLayout->hasResilientSuperclass()
        ? MetadataLayout->getRelativeVTableOffset()
        : MetadataLayout->getStaticVTableOffset();
    B.addInt32(offset / IGM.getPointerSize());
    B.addInt32(VTableEntries.size());

    for (auto fn : VTableEntries)
        emitMethodDescriptor(fn);
}
  • 在该函数中,首先拿到当前descriptor的内存偏移,这个偏移量是 TargetClassDescriptor 这个结构中的成员变量所有内存大小之和,并且在最后还拿到了 VTableEntries.size()
  • 然后在这个偏移位置开始添加方法。
  • 总结:虚函数表的内存地址,是 TargetClassDescriptor 中的最后一个成员变量,添加方法的形式是追加到数组的末尾。所以这个虚函数表是按顺序连续存储类的方法的指针。

extension中方法调用

  • 在原有Test类基础上添加extension,并添加test2方法
extension Test {
    func test2() {
        
    }
}
  • 通过汇编查看
image
  • 我们发现它并没有获取metedata,进行偏移的方式来获取函数地址,而是通过地址直接进行调用,也就是采用静态派发方式。
  • 这里方法为什么没有添加到函数表中呢?
    • 一方面是类是可以继承的,如果给父类添加extension方法,继承该类的所有子类都可以调用这些方法。并且每个子类都有自己的函数表,所以这个时候方法存储就成为问题。
  • 所以为了解决这个问题,直接把 extension 独立于虚函数表之外,采用静态调用的方式。在程序进行编译的时候,函数的地址就已经知道了。

修饰函数的关键字

  • final: 添加了final关键字的函数无法被写, 使用静态派发, 不会在vtable中出现, 且对objc运行时不可见。 如果在实际开发过程中,属性、方法、类不需要被重载的时候,可以添加final关键字。

  • dynamic: 函数均可添加dynamic关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。

  • @objc: 该关键字可以将swift函数暴露给Objc运行时, 依旧是函数表派发。

  • @objc + dynamic: 消息发送的方式。

总结

  • Swift中的方法调用分为静态派发动态派发两种
  • 值类型中的方法就是静态派发
  • 引用类型中的方法就是动态派发,其中函数的调度是通过V-Table函数表来进行调度的
类型 调度方式 extension
值类型 静态派发 静态派发
函数表派发 静态派发
NSObject子类 函数表派发 静态派发

相关文章

  • Swift底层原理-方法调度

    Swift底层原理-方法调度 我们知道,在OC中方法的调用是通过objc_msgSend来发送消息的;那么在Swi...

  • Swift进阶(三)—— 方法调度

    该系列主要是记录Swift中与OC底层差异。该篇主要是关于各种方法调度的差异。 前面我们研究了结构体和类的底层结构...

  • golang

    golang携程调度,runtime包 golang内存模型 csp原理 context的原理 slice底层结构...

  • iSO底层原理 - load方法和initialize方法的区别

    iSO底层原理 - load方法iSO底层原理 - initialize方法 load 和 initialize方...

  • Swift 底层原理初探

    Swift 底层原理初探 1. 编译原理 在iOS中我们经常使用Objective-C和Swift这两门语言进行编...

  • swift进阶一:源码编译

    从今天开始,进入swift学习篇章。 想了解OC底层原理,可查看? OC底层原理 学习大纲[https://www...

  • Swift方法调度

    Struct: 在结构体中方法都是静态调用(直接调用),也就意味着在在编译连接完成之后当前这个函数的地址就已经确定...

  • Swift 方法调度

    在swift中方法调度分为两种,直接调用和查找调用struct结构体的方法调用方式为直接调用,直接调用函数地址cl...

  • Swift底层原理-类与对象

    Swift底层原理-类与对象 准备工作 该系列文章,主要通过Swift底层源码进行研究。 可以通过该网址下载Swi...

  • Swift 方法(函数)调度

    Swift 方法(函数)调度 [TOC] 1. 前言 由于Objective-C是一门动态语言,方法的调度中主要是...

网友评论

      本文标题:Swift底层原理-方法调度

      本文链接:https://www.haomeiwen.com/subject/fosqqdtx.html