Swift 方法(函数)调度

Swift 方法(函数)调度

[TOC]

1. 前言

由于Objective-C是一门动态语言,方法的调度中主要是消息查找和消息转发。那么对于静态的Swift中的方法是如何调度的呢?下面我们就来一起探索一下。

2. 静态派发

首先还是需要了解一下Swift中的值类型和引用类型。

对于值类型对象的函数的调用方式是静态(直接)派发(调用),也可以说是直接地址调用(指针调用),也就是说,在编译,链接完成后当前函数的地址在Mach-O代码段就已经有了确定的位置。

Swift中典型的值类型就是结构体,那么我们就通过如下的分析,证明结构体中函数的调用是静态调用。

首先我们编写如下代码:

struct Teacher {
    func teach() {}
}

var t = Teacher()
t.teach()

print("end")

2.1 通过汇编看函数的调用

在函数调用处添加断点

开启汇编调试Xcode->Debug->Debug Workflow->Always Show Disassembly

运行结果如下:

image

我们可以看到,在这里直接是call这个地址,其实这就足以说明是直接地址调用。

2.2 通过MachOView进一步探索

2.2.1 汇编指令的查找

下面我们将可执行文件拖拽到MachOView中。

image

在MachOView`中的代码段(__text),由于我们的代码很少,很容易就找了这段汇编指令,如下图所示。

image

2.2.2 符号的查找

2.1中的汇编代码中,我们可以看到后面的注释直接标示处理函数的符号,那么这个符号存储在Mach-O中的什么位置呢?

首先我们来到符号表Symbol Table中查找一下:

PS: 其实还有个Dynamic Symbol Table,这是存储动态库中一些符号的偏移信息的,这里并不是我们要查找的目标。

image

我们知道字符串是存储在字符串表的,其中包含变量的名称,函数的名称等,那么我们就可以根据符号表中的偏移值到字符串表中查找相应的字符。

image

由于代码不多,在字符串边也很容易就找到了我们的字符串,偏移值是2,所以排除第一空格,第二个.往后面就是我们要查找到的符号的首个字符了。

其实我们还可以通过nm命令去查找符号,首先cd 到Mach-O文件的路径使用nm xxxx来查看当前Mach-O中的符号,比如我们这里的符号就是:

image

当然这些符号是通过命名重整的,如果想要得到和2.1中那样的符号可以通过xcrun swift-demangle 符号这个命令进行还原符号。比如上图中的第一个符号:(注意:要去掉前面的_$)

image

关于符号的查找,其实在release环境中并不会像上面一样查找得到。我们更换为release环境,编译。

我们打开可执行文件的目录:

image

在这里多了一个dSYM文件。

我们再次将Mach-O文件拖拽到MachoView中打开,首先我们会看到如下的结果:

image

这里多了个Fat Header,并且有了x86_64ARM64两种架构的可执行文件,这是为什么呢?其实就是release环境会包含所有支持的架构的可执行文件,详细的介绍可以看我这篇文章Mach-O探索,下面我们打开x86_64这个可执行文件进行查找,因为我的电脑还是这个架构的,不是M1的。

我们在符号表Symbol Table继续搜索teach,结果如下:

image

根据上图我们可以看到并没有搜索到任何结果,我们去字符串表(String Table)进行查看也同样没有teach相关的结果。其实主要原因是静态链接的函数在release环境是不需要使用符号的,在编译完成后,其地址就已经确定了,此时也不需要调试了,留着符号就会过多的占用存储空间,增加可执行文件的体积,那么还留在符号表干嘛呢?其实对于静态函数和一些确定的可以不需要存储符号,但是那些不确定的符号,比如说一些动态加载的库和函数,就需要用到符号了,所以保留符号表还是非常有用的。

对于不能确定的符号,比如说print,如果我们不调用它,就不需要加载它,在运行的时候,我们通过print打印一些数据,就会将它加载到内存,相当于懒加载,此时会通过dyldbind操作将其进行绑定,(在Xcode 12.2中我并没有测试出来,‍♂️),但是相关原理还是没问题的,关于dyld的加载,也可以参考我的另一篇文章iOS 应用加载dyld篇。

其实关于符号在不同语言中是不一样的,在CObjective-C中我们是不能使用同名方法的,原因就是他们对于命名重整的规则比较简单。

C

#include 

void test(){
    printf("test");
}
image

我们可以看到在C语言中命名重整就是在函数名称前面加个_,所以对于C语言来说,如下的写法是会报编译错误的:

image

Objective-C

@interface Test: NSObject

@end

@implementation Test

-(void)oc_test{
    NSLog(@"oc_test");
}

@end
image

我们可以看到在OC中命名重整就是在使用-[类名 方法名],如果是类方法就是+[类名 方法名],所以对于OC来说,同名的类方法和对象方法是可以同时存在的,但是如下的写法是会报编译错误的:

image

对于Swift来说,使用的是运行函数重载,可以使用同名不同参的函数,因为它的命名重整规则很复杂,可以确保函数符号的唯一性。

2.2.3 函数地址

在上面测试中我们看到汇编代码中call的地址和Mach-O文件中的地址是一致的,其实在iOS手机中运行的地址是不一致的,下面我们编写一个iOS APP。

代码如下:

import UIKit

struct Teacher {
    func teach() {}
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var t = Teacher()
        t.teach()

        print("end")
    }
}

使用真机运行,在汇编代码和Mach-O中查看,结果如下图:

image

可以明显的看出两个的地址是不一样的。那么这是为什么呢?其实就是iOS系统为了安全性,使用了ASLR,即地址空间配置随机化,英文全称是(Address space layout randomization)。

简单来说就是利用随机的方式配置一个数据地址空间,这样攻击者就不会轻易的找到特定的内存地址来进行攻击。其实我们常见的操作系统级别都已经实现了ASLR,详情请自行百度。下面我们就通过计算得出内存地址。

首先在iOS这边查看ASLR需要通过image list命令。

image

这里我们得到的随机加载首地址是:0x0000000102da8000,我们让Mach-O中的函数地址加上ASLR,即:

0000000100007844 + 0x0000000102da8000 = 0x202DAF844

这与我们的实际调用地址还是不一样啊?其实在Mach-O中还有个虚拟地址,在Mach-OLoad Commands段,存放了各段的虚拟内存地址和偏移量等信息。

我们打开Mach-OLoad Commands中的LC_SEGMENT_64(__PAGEZERO):

image

我们可以看到VM Address是0,VM Size0000000100000000File OffsetFile Size也是0,说明我们在符号表看到的地址是相对Mach-O中首地址做了加虚拟地址和大小的偏移的,这也是Mach-O中做的类似安全性的一种方案,所以去除掉这段虚拟的偏移就是我们想要的真实的内存地址了。

0000000100007844 + 0x0000000102da8000 - 0000000100000000 = 0x102DAF844

其实我们还可以通过image list -o -f命令直接拿到去除掉这段偏移的ASLR

image

0000000100007844 + 0x0000000002da8000 = 0x102DAF844

2.4 小结

经过上面一番的探索,总结如下:

  1. 无论是在Mac OS中的应用程序(call 0x100003e50)还是iOS真机(bl 0x102DAF844),在汇编代码上都是直接调用一个地址
  2. 在此我们也通过Mach-O文件查找到了函数地址的存储
  3. 还在iOS真机环境通过ASLR计算得到了真实的函数地址
  4. 所以地址调用就是直接调用,也就是静态调用
  5. 至此,在Swift结构体struct(值类型)中的函数是静态调用基本证明完毕

3. 动态派发

既然有静态,那么当然就会有动态,在Swift中的引用类型数据代表类中的函数调用就是动态派发。下面我们来一起看看。

3.1 vtable 简介

3.1.1 简介

首先介绍一下Swift中的vtableSIL文件中的格式:

//声明sil vtable关键字
decl ::= sil-vtable
//sil vtable中包含 关键字、标识(类名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法 包含了声明以及函数名称
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na
me

3.1.2 示例

Swift 示例代码:

class Teacher {
    func teach() {}
    func teach1() {}
    func teach2() {}
    func teach3() {}
    func teach4() {}
}

cd到文件目录,执行如下命令,生成sil文件并打开:

rm -rf main.sil && swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil

sil 代码:

class Teacher {
  func teach()
  func teach1()
  func teach2()
  func teach3()
  func teach4()
  @objc deinit
  init()
}

sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> ()   // Teacher.teach()
  #Teacher.teach1: (Teacher) -> () -> () : @main.Teacher.teach1() -> () // Teacher.teach1()
  #Teacher.teach2: (Teacher) -> () -> () : @main.Teacher.teach2() -> () // Teacher.teach2()
  #Teacher.teach3: (Teacher) -> () -> () : @main.Teacher.teach3() -> () // Teacher.teach3()
  #Teacher.teach4: (Teacher) -> () -> () : @main.Teacher.teach4() -> () // Teacher.teach4()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

以上就是Teacher这个类的函数表。除了我们自定义的5个方法,还有initdeinit两个函数。

3.2 引入vtable

上面我们提到了vtable的概念,其实是开了上帝视角,那么对于一个不知道vtable的人该如何进行探索呢?那么我们首先向到的就是通过汇编代码,下面我们就编写如下代码:

import UIKit

class Teacher {
    func teach() {}
    func teach1() {}
    func teach2() {}
    func teach3() {}
    func teach4() {}
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var t = Teacher()
        t.teach()
        t.teach1()
        t.teach2()
        t.teach3()
    }
}

我们使用真机环境运行,Arm64汇编,对于函数的调用,我们首先就去找bl相关的指令。

image

我们可以看到在截图中:

  • 每次跳转的时候都是跳转到x9寄存器
  • x9寄存器的值每次都是通过将x8寄存器中的值读取到x9在偏移得到的
  • 每次偏移都是8字节
  • x8寄存器中存储的值是由sp向下偏移来的,其实就是t对象的地址,这里就不通过汇编分析了,直接通过register read x8里面的值,此时就可以发现确实时t对象的地址,在左侧可以看到。

我们在blr x9的地方按住control点击step into跳转进行。此时我们发现这里就是我们调用的方法,示例如下:

image

按照几次跳转偏移的值,我们可以看到这些方法在内存中的排列是连续的,并且地址都是基于对象的地址进行偏移的。很像一个数组的index取值一样。其实跟表也很像了。

3.3 源码分析

根据上面的分析,我们可以想到肯定有那么一个方法,初始化了这个函数表,我们在源码中找到了initClassVTable这个函数。

initClassVTable 源码:

/// Using the information in the class context descriptor, fill in in the
/// immediate vtable entries for the class and install overrides of any
/// superclass vtable entries.
static void initClassVTable(ClassMetadata *self) {
  const auto *description = self->getDescription();
  auto *classWords = reinterpret_cast(self);

  if (description->hasVTable()) {
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    auto descriptors = description->getMethodDescriptors();
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
      auto &methodDescription = descriptors[i];
      swift_ptrauth_init(&classWords[vtableOffset + i],
                         methodDescription.Impl.get(),
                         methodDescription.Flags.getExtraDiscriminator());
    }
  }

  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(descriptor.Class.get());
      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.
      auto baseClassMethods = baseClass->getMethodDescriptors();

      // If the method descriptor doesn't land within the bounds of the
      // method table, abort.
      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();
      auto offset = (baseVTable->getVTableOffset(baseClass) +
                     (baseMethod - baseClassMethods.data()));

      swift_ptrauth_init(&classWords[offset],
                         descriptor.Impl.get(),
                         baseMethod->Flags.getExtraDiscriminator());
    }
  }
}

initClassVTable函数中,主要有两部分,第一部分就是判断当前类的VTable,第二部分就是判断继承的VTable

  1. 当前类的VTable,通过一个for循环,在VTableSize内,取出方法存储到连续的内存中。
  2. 对于继承的方法,原理类似,只不过增加了继承自哪个类的一些处理

我们编写简单代码,并添加断点,进行测试,可以发现确实会调用initClassVTable函数进行存储方法。

image

4. 类中函数调用的详细探索

虽然在上面我们验证了Swift中类的方法调用是函数表调用,但是类中的发放有很多的组合,其中不乏有:

  1. extension 中的方法
  2. 继承的方法
  3. final 修饰的方法
  4. classstatic修饰的类方法
  5. @objc 修饰的方法时与OC的交互
  6. dynamic 修饰的方法
  7. @objc + dynamic 修饰的方法

下面我们一一探索一下。

4.1 extension 中的方法

在类中的方法会存储到VTable中,那么我们写的extension中的方法会存储到哪里呢?我们添加如下代码:

extension Teacher {
    func teach2() {}
}

编译为sil

class Teacher {
  func teach()
  @objc deinit
  init()
}

extension Teacher {
  func teach2()
}

sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> ()   // Teacher.teach()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

我们在VTable中并没有找到teach2这个函数。那么extension中的方法是如何调用的呢?我们尝试调用一下这个方法,看看汇编代码中是什么样子的。

image

我们可以看到,对于extension中的函数,使用的是直接调用(静态调用)。

那么这是为什么呢?

我猜想是,由于extension可以编写在任何地方,在编译连接的时候会有先后顺序,不可能保证把extension中的方法也顺序的加载到VTable中。而且像String等系统库中的类,都是一些动态库,更不可能因为我们加了个extension就重新将方法添加到VTable中。

关于子类是否会继承父类的extension中的VTable在下一节讲述。

4.2 继承的方法

OC中对于继承的方法会不断的通过isa想父类进行查找,那么Swift中会是怎么样呢?我们编写如下代码:

class Teacher {
    func teach() {}
}

class Student: Teacher {
    func study() {}
}

编译后的sil代码:

class Teacher {
  func teach()
  @objc deinit
  init()
}

@_inheritsConvenienceInitializers class Student : Teacher {
  func study()
  @objc deinit
  override init()
}

sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> ()   // Teacher.teach()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

sil_vtable Student {
  #Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () [inherited]   // Teacher.teach()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Student.__allocating_init() -> main.Student [override]   // Student.__allocating_init()
  #Student.study: (Student) -> () -> () : @main.Student.study() -> ()   // Student.study()
  #Student.deinit!deallocator: @main.Student.__deallocating_deinit  // Student.__deallocating_deinit
}

我们看到在Studentvtable中居然有teach方法,这就说明在Swift中:

  1. 继承的时候会将父类VTable中的方法继承到子类的VTable
  2. 这是一种以空间换时间的方式,减少了查找的时间

那么会不会继承extension中的方法呢?其实是不会的,下面我们来验证一下:

在刚才的代码中添加如下代码:

extension Teacher {
    func teach2() {
    }
}

我们直接看studentVTable

sil_vtable Student {
  #Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> () [inherited]   // Teacher.teach()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Student.__allocating_init() -> main.Student [override]   // Student.__allocating_init()
  #Student.study: (Student) -> () -> () : @main.Student.study() -> ()   // Student.study()
  #Student.deinit!deallocator: @main.Student.__deallocating_deinit  // Student.__deallocating_deinit
}

此时我们并没有看到studentVTable中有teach2这个函数,所以子类并不会继承父类extension中的函数到自己的VTable,在调用过程中依旧会是静态调用。其实原理还是那样,extension的添加时机不确定,VTable中的函数是顺序排列的,对于父类和子类都会存在添加到函数表时无法确定插入位置,另外也有可能存在先继承后extension的情况。所以继承的时候extension中的函数也是静态的。

4.3 使用final修饰的方法

测试代码:

class Teacher {
    final func teach() {}
}

sil代码:

class Teacher {
  final func teach()
  @objc deinit
  init()
}

sil_vtable Teacher {
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

此时我们在VTable中并没有找到teach方法,其实这里使用final修饰后就成了静态调用了。

我们调用一下,根据汇编代码的结果,成功验证了其静态调用的特性。

image

4.4 class和static修饰的类方法

Swift中我们会使用classstatic修饰方法,使其能够直接被类调用。也就是我们常用的类方法。那么这两种方法是如何存储的呢?下面我们就来看看。

测试代码:

class Teacher {
    class func eat() {
        print("eat")
    }

    static func drink() {
        print("drink")
    }
}
Teacher.eat()
Teacher.drink()

print("end")

sil代码:

class Teacher {
  class func eat()
  static func drink()
  @objc deinit
  init()
}

sil_vtable Teacher {
  #Teacher.eat: (Teacher.Type) -> () -> () : @static main.Teacher.eat() -> ()   // static Teacher.eat()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

此时我们在VTable中只看见了eat方法,并且后面的注释是static Teacher.eat(),所以说使用classstatic修饰的类方法本质上都是static,但是唯一的区别是,class修饰的类方法存储在VTable中,static修饰的类方法是以静态方法的形式存储的。下面我们在通过汇编代码看一看。

image

这里我们可以看到,对eatdrink的调用都是直接地址调用。那么eat方法也是静态存储的吗?这个目前我没能进一步验证。后续好好思考再看看,如有高见,感谢告知!谢谢!。

4.5 @objc 修饰的方法

使用@objc修饰是将方法暴露给oc

image

添加@objc后,编译器会提醒你引入Foundation

测试代码:

class Teacher {
    @objc func teach() { print("teach") }
    func teach1() { print("teach1") }
}

print("end")

sil代码:

class Teacher {
  @objc func teach()
  func teach1()
  @objc deinit
  init()
}

sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> ()   // Teacher.teach()
  #Teacher.teach1: (Teacher) -> () -> () : @main.Teacher.teach1() -> () // Teacher.teach1()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

通过sil代码我们可以发现,使用@objc修饰的方法依旧存在VTable中。

特别注意:

// @objc Teacher.teach()
sil hidden [thunk] @@objc main.Teacher.teach() -> () : $@convention(objc_method) (Teacher) -> () {
// %0                                             // users: %4, %3, %1
bb0(%0 : $Teacher):
  strong_retain %0 : $Teacher                     // id: %1
  // function_ref Teacher.teach()
  %2 = function_ref @main.Teacher.teach() -> () : $@convention(method) (@guaranteed Teacher) -> () // user: %3
  %3 = apply %2(%0) : $@convention(method) (@guaranteed Teacher) -> () // user: %5
  strong_release %0 : $Teacher                    // id: %4
  return %3 : $()                                 // id: %5
} // end sil function '@objc main.Teacher.teach() -> ()'

sil代码中我们可以看到,对于使用@objc修饰的方法,实际上是生成了两个方法,其中一个就是我们Swift中原有的方法,另一个就是如上面代码所示的@objc方法,并在其内部调用了Swift原有的方法。

所以使用@objc修饰的方法本质是,通过sil代码中的@objc方法调用,Swift中的方法来实现的。

汇编代码分析:

image

通过汇编代码我们也可以看到使用@objc修饰的方法也是函数表调用。

那么通过OC调用是什么样呢?我们新建一个OC的命令行项目。在新建一个Test.swift文件,此时会提示创建桥接文件,我们直接创建桥接文件,然后在.swift文件中添加如下代码:

class Teacher {
    @objc func teach() {}
}

我们来到main.m文件中引入混编的头文件,那么这个头文件是什么呢?其实是:projectName-Swift.h,我们点击一个swift文件,此时如下图:

image

那么此时我们就可以调用声明的Swift类了吗?答案是不可以的,如下图所示:

image

要想在OC中使用Swift中的类,其实还需要Swift代码继承自NSObject。继承后就不会报错了:

image

其实我们在桥接文件(按照上面截图中的方法找到)中也可以看到:

image

如果不继承自NSObject则不会有上图所示的代码。

综上所述:

  1. 使用@objc修饰的方法是为了暴露给OC使用
  2. 使用@objc修饰的方法依旧存储在VTable
  3. 底层实现是生成两个方法,一个普通方法,一个使用@objc修饰的方法,在@objc方法中调用这个普通方法
    1. 要想在OC中使用Swift类,还需其继承自NSObject

4.6 dynamic修饰的方法

下面我们来看看使用dynamic修饰的方法。

测试代码:

class Teacher {
    dynamic func teach() {
        print("teach")
    }
}

sil代码:

class Teacher {
  dynamic func teach()
  @objc deinit
  init()
}

sil_vtable Teacher {
  #Teacher.teach: (Teacher) -> () -> () : @main.Teacher.teach() -> ()   // Teacher.teach()
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

通过sil代码我们可以看到,使用dynamic修饰的方法是存放在VTable中的。

汇编代码:

image

通过汇编代码我们也可以看到时函数表调用。

目前看来跟我们直接写的方法没什么区别,那么使用dynamic修饰的函数有什么用呢?

使用dynamic修饰的方法也会有动态性,在Swift中可以使用@_dynamicReplacement(for: xxx)将使用dynamic修饰的方法进行替换,比如:

class Teacher {
    dynamic func teach() {
        print("teach")
    }
}

extension Teacher {
    @_dynamicReplacement(for: teach)
    func teach1() {
        print("teach1")
    }
}

此时我们调用teach方法就会打印teach1。如果需要被替换的方法没有使用dynamic修饰会报编译错误。这里就不贴图了,自己试试就行。

4.7 @objc + dynamic修饰的方法

通过上面的分析我们知道在Swift中使用@objc修饰方法是将Swift方法暴露给OC,使用dynamic修饰,可以替换方法。那么同时使用这两修饰方法呢?下面我们就来一起看看。

测试代码:

class Teacher {
    @objc dynamic func teach() {
        print("teach")
    }
}

sil代码:

class Teacher {
  @objc dynamic func teach()
  @objc deinit
  init()
}

sil_vtable Teacher {
  #Teacher.init!allocator: (Teacher.Type) -> () -> Teacher : @main.Teacher.__allocating_init() -> main.Teacher  // Teacher.__allocating_init()
  #Teacher.deinit!deallocator: @main.Teacher.__deallocating_deinit  // Teacher.__deallocating_deinit
}

此时在VTable中就没有我们的方法了。在sil代码中也会同时生成teach方法和@objc teach方法。并且在@objc teach方法中会调用teach方法,这与我们分析@objc时是一致的。

汇编代码:

image

此时我们可以看到,这时候的方法调用已经是objc_msgSend了,这就是Objective-C中的消息发送流程了,关于消息发送的详细解释请看我的另一篇文章iOS Objective-C 消息的查找

关于动态性,是不是可以通过runtimemethodSwizzling进行方法交换呢?下面我们来验证一下。继续使用上面创建的Objective-C工程,并新建一个OC类,代码如下:

#import 

NS_ASSUME_NONNULL_BEGIN

@interface TestObjectC : NSObject
-(void)study;
@end

NS_ASSUME_NONNULL_END

#import "TestObjectC.h"
#import 
#import "TestOC-Swift.h"

@implementation TestObjectC

+ (void)load{
    
    Method oriMethod = class_getInstanceMethod([Teacher class], @selector(teach));
    Method swiMethod = class_getInstanceMethod([self class], @selector(study));
    method_exchangeImplementations(oriMethod, swiMethod);
    
}

-(void)study{
    NSLog(@"study");
}

@end

Swift代码:

import Foundation

class Teacher: NSObject {
    @objc dynamic func teach() {
        print("teach")
    }
}

main中的代码:

#import 
#import "TestOC-Swift.h"
#import "TestObjectC.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Teacher *te = [Teacher new];
        [te teach];
        
        TestObjectC *t = [TestObjectC new];
        [t study];
    }
    return 0;
}

打印结果:

image

根据打印结果我们可以看到方法交换成功。其实去掉dynamic修饰,只保留@objc修饰也可以交换成功。所以说在Swift中使用dynamic修饰方法更多的作用还是为Swift提供了方法交换的特性。

5. 总结

至此我们对Swift中的方法的分析就到此为止了,下面总结一下:

  1. Swift中的方法调用分为静态和动态两种
  2. 值类型(例如struct)中的方法就是静态调度
  3. 引用类型(例如class)中的方法调度主要是通过函数表VTable来进行调度,也可以说是动态调度
  4. class中,extension中的方法是静态调度
  5. class中,使用final修饰的方法是静态调度
  6. class中,使用classstatic修饰的方法也是静态调度
  7. class中,虽然使用class修饰的方法是静态调度,但是会存储在VTable中,这点还没仔细分析。
  8. class中,使用@objc修饰的方法也是函数表调度,如果需要被OC调用还需继承自NSObject
  9. class中,使用@objc修饰的方法可以在OC中使用methodSwizzling进行方法交换
  10. class中,使用dynamic修饰的方法也是函数表调度,可以在Swift中进行方法替换
  11. class中,同时使用dynamic@objc修饰的方法的调度方式是objc_msgSend

你可能感兴趣的:(Swift 方法(函数)调度)