Swift方法调用

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

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

推荐阅读更多精彩内容