Swift底层原理-方法调度

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子类 函数表派发 静态派发
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容