Swift进阶杂谈4:方法调度

2,640 阅读4分钟

通过之前的分析,结构体是值类型,类是引用类型。那结构体和类的方法存储在哪里?我们分析一下

静态派发

值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后,当前函数的地址就已经确定了,拿在执行代码的过程中就直接跳转到这个地址来执行当前对应的方法,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用

动态派发

类中声明的方法是通过V-table来进行调度的。 V-TableSIL中的表示是这样的:

//声明sil vtable关键字
1 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

我们通过一个简单的例子来看一下

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

通过SIL源文件查看其在SIL中的v-table,如下图所示

由上图可知

  • sil_vtable:关键字
  • LGTeacher:表明当前是LGTeacher class的函数表
  • 其次就是当前方法的生命对应着方法的名称
  • 本质:函数表的本质就类似于我们理解的数组,声明在class内部的方法在不加任何关键字修饰的过程中,连续存放在我们当前的地址空间

通过查看源码发现其内部是通过for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,从这里可以印证函数是连续存放的。

可以得出结论:对于class中函数来说,类的方法调度是通过V-Taable,其本质就是一个连续的内存空间(数组结构)

函数声明位置的不同也会导致派发方式的不同。如果我们在类的扩展中声明的函数,这里就是一个直接调用。

其原因是因为子类将父类的函数表全部继承了,如果此时子类增加函数,会继续在连续的地址中插入,假设extension函数也在函数表中,则意味着子类也有,但是子类无法并没有相关的指针记录函数是父类方法还是子类方法,所以不知道方法该从哪里插入,导致extension中的函数无法安全的放入子类中。所以在这里可以侧面证明extension中的方法是直接调用的,且只属于类,子类是无法继承的

开发注意点:

  • 继承方法和属性,不能写extension中。
  • 而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写

扩展 : final、@objc、dynamic修饰函数

final修饰

  • final修饰的方法是直接调度@objc修饰
  • 使用@objc关键字是将swift中的方法暴露给OC,@objc修饰的方法是函数表调度
  • 如果只是通过@objc修饰函数,OC还是无法调用swift方法的,因此如果想要OC访问swift,class需要继承NSObject
  • 查看SIL文件发现被@objc修饰的函数声明有两个:swift+OC,即在SIL文件中生成了两个方法
    • swift原有的函数
    • @objc标记暴露给OC来使用的函数:内部调用swift的

dynamic修饰 使用dynamic的意思是可以动态修改,以为着当继承自NSObject时,可以使用method-swizzling

场景:swift中实现方法交换 在swift中的需要交换的函数前,使用dynamic修饰,然后通过:@_dynamicReplacement(for: 函数符号)进行交换,如下所示

class PDTeacher: NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

extension PDTeacher{
    @_dynamicReplacement(for: teach)
    func teach5(){
        print("teach5")
    }
}

teach方法替换成了teach5

总结

  • struct值类型,其中函数的调度属于直接调用地址,即静态调度
  • class引用类型,其中函数的调度是通过V-Table函数表来进行调度的,即动态调度
  • extension中的函数调度方式是直接调度
  • final修饰的函数调度方式是直接调度
  • @objc随时的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject
  • dynamic修饰的函数的调度方式是函数表调度,是函数具有动态性。
  • @objc + dynamic组合修饰的函数调度,是执行的是objc_msgSend流程,即动态消息转发