Swift方法调用

作者: 正_文 | 来源:发表于2022-08-26 22:34 被阅读0次

OC作为动态语言,方法调用,是通过消息发送机制void objc_msgSend(id self, SEL cmd,…);第一个参数是接受消息的对象;第二个是消息本身,方法的名字后面的就是消息中的那些参数
Swift静态语言,和OC不同,方法的调用有2种方式:静态调用 & 动态调用

一、静态调用

静态调用,即直接地址调用,调用函数指针,这个函数指针在编译链接完成后就已经确定了,存放在代码段。结构体是值类型,内部并不存放方法,因此直接通过地址直接调用。

struct Teacher{
    func teach() { }
}
var t = Teacher()
t.teach()
image.png
断点调试,可以看到是直接地址调用。打开Mach-O__text段,就是所谓的代码段,需要执行的汇编指令都在这里:
image.png
那地址后面的符号(重整后方法名)地址:String Table首地址 + 偏移量
image.png
命名重整:工程名+类名+函数名,但符号表中并不存储字符串,字符串存储在String Table(字符串表,存放了所有的变量名函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符。

当然我们还可以通过终端查看项目符号表:

image.png
nm Mach-O:查看符号表
grep:管道输出
xcrun swift-demangle 符号:还原符号名称
函数重载

c语言-重整,所以无法区分同名函数,即无法方法重载

image.png
c语言,函数名字一样,会直接报错。
image.png
OC中同一作用域内不允许相同函数名,严格意义上也不能实现函数重载,如下:
oc代码:
- (void)eat{}
- (void)eat:(NSString *)name{}
//- (void)eat:(int)age{} //报错:Duplicate declaration of method 'eat:'
+ (void)eat{}

Mach-O符号:
- [Animal eat]
- [Animal eat:]
//- [Animal eat:]
+ [Animal eat]

Swift通过命名重整技术,使方法名符号复杂,保证方法符号的唯一,因此允许方法重载

二、动态调用—类的方法

代码:

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

SIL代码:

sil_vtable Teacher {
  #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.teach5: (Teacher) -> () -> () : @main.Teacher.teach5() -> () // Teacher.teach5()
  #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类的函数表函数表 可以理解为数组,声明在class内部的方法在不加任何关键字修饰的过程中,是连续存放在我们当前的地址空间中的
V-Table源码:

static void initClassVTable(ClassMetadata *self) {
  const auto *description = self->getDescription();
  // 可以看成是Metadata地址
  auto *classWords = reinterpret_cast<void **>(self);

  if (description->hasVTable()) {
    //  获取vtable的相关信息
    auto *vtable = description->getVTableDescriptor();
    auto vtableOffset = vtable->getVTableOffset(description);
    // 获取方法描述集合
    auto descriptors = description->getMethodDescriptors();
    // &classWords[vtableOffset]可以看成是V-Table的首地址
    // 将方法描述中的方法指针按顺序存储在V-Table中
    for (unsigned i = 0, e = vtable->VTableSize; i < e; ++i) {
      auto &methodDescription = descriptors[i];
      swift_ptrauth_init_code_or_data(
          &classWords[vtableOffset + i], methodDescription.getImpl(),
          methodDescription.Flags.getExtraDiscriminator(),
          !methodDescription.Flags.isAsync());
    }
  }

  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<ClassDescriptor>(descriptor.Class.get());
      //  指向原来(基类)的MethodDescriptor地址
      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.
      // 基类的MethodDescriptors
      auto baseClassMethods = baseClass->getMethodDescriptors();

      // If the method descriptor doesn't land within the bounds of the
      // method table, abort.
      // 如果baseMethod不符合在基类的MethodDescriptors中间,报错
      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();
       // 基类的vTable地址 + baseMethod在baseClassMethods的index???
      auto offset = (baseVTable- >getVTableOffset(baseClass) +
                     (baseMethod - baseClassMethods.data()));
      swift_ptrauth_init_code_or_data(&classWords[offset],
                                      descriptor.getImpl(),
                                      baseMethod->Flags.getExtraDiscriminator(),
                                      !baseMethod->Flags.isAsync());
    }
  }
}

创建方法主要分成两部分:

  1. 获取vtable信息,获取方法descriptions,将方法Description的指针Imp(未重写的)存储在V-Table(元数据地址 + vtableOffset)中。
  2. 获取OverrideTable信息,获取overrideDescriptors,将description的指针Imp(重写的)存储在V-Table(offset )中,此处的offset为基类的vTable地址 +baseMethod在baseClassMethods的index
    即、一个类的V-Table是由自身方法重写方法(父类方法)组成,对比OC需要去父类去查找,Swift用空间换时间,提高了查找效率。

关于 extension继承方法和属性,不能写extension中。而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写
扩展的函数并没有在函数表中。新建子类,通过sil一样会发现,子类函数表中只会继承父类函数表中的函数。为什么呢?
因为,子类将父类的函数表全部继承了,此时子类增加函数,那么就继续在连续的地址中insert。如果extension函数也是在函数表中,则意味着子类也有父类extension中声明的函数,但是子类并没有相关的指针记录函数是父类方法还是子类方法,所以子类方法不知道该insert到哪里,导致extension中的函数无法安全的放入子类中.
所以,扩展的方法是直接(静态)调用,并只属于当前类,子类只能使用,无法继承和重写

验证

调用方法的地方,打上断点,打开汇编:

企业微信截图_929e536b-0e6b-43d8-bbf4-c5b1c3d455bc.png
观察发现,对象指针每次偏移8字节找到方法,如[x8, #0x58][x8, #0x60],然后调用。而不是,直接调用方法地址的,参考原文静态调用部分。

关于汇编,arm64汇编指令

  • mov:将某一寄存器的值复制另一寄存器(只能用于寄存器与寄存器,或者寄存器与常量之间 传值,不能用于内存地址),
    例,mov x1, x0寄存器x0的值复制寄存器x1中;
  • ldr:将内存中的值读取到寄存器中,
    例,ldr x0, [x1, x2]寄存器x1寄存器x2 相加作为地址,取该内存地址的值翻入寄存器x0中;
  • str:将寄存器中的值写入到内存中,
    例,str x0, [x0, x8]寄存器x0的值 保存内存[x0 + x8]处;
  • blr:带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址;
  • bl跳转到某地址。

三、final、@objc、dynamic修饰函数

final 修饰的方法是 直接调度
@objc 关键字是将swift中的方法暴露给OC,函数表调度;
@objc + NSObject @objc修饰函数,OC还是无法调用swift方法的,因此如果想要OC访问swift,class需要继承NSObject;
dynamic:可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling,走的是objc_msgSend流程,即 动态消息转发
@_dynamicReplacement(for: 函数符号)进行方法交换。

关于方法的调度,更详细可以参考文章 Swift 底层是怎么调度方法的

method dispatch.png

相关文章

网友评论

    本文标题:Swift方法调用

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