理解 Swift 中的方法派发机制 - 消息派发

消息派发是 Swift 动态派发方法中灵活性最高的派发方式。它非常灵活,方法的实现可以在运行时通过调整来更改。实际上它甚至不使用 Swift ,它是存在于 Objective-C 运行时库中。

消息派发函数调用是使用 ObjC 运行时的 objc_msgSend 函数派发的。 Objective-C 类的实例有一个 isa 指针,它指向"类对象"------内存中类型的实现。

objc_msgSend 通过 isa 找到类,然后检查其方法选择器表。如果找到该方法,则会执行该方法。如果不是,则运行时将跟随指针指向超类中的表。如果还未找到该方法,则运行时将继续遍历对象层次结构树,直到找到该方法或到达 NSObject(ObjC 的基类)。

这个方法选择器表被实现为消息传递字典,它在运行时是可变的。这就是 ObjC 的 method-swizzling 实现本质。ObjC 运行时会在使用类方法时缓存它们的内存地址。调用缓存方法几乎与常规派发函数调用一样快,一旦程序运行一段时间并且缓存足量的方法,消派发就会相当快。

消息派发的 SIL

要在 Swift 中使用消息派发,你需要两个关键字:

  • @objc 属性,告诉编译器将类、属性或方法提供给 Objective-C 运行时。
  • dynamic 关键字,告诉编译器通过消息派发的方式调用属性或方法。

示例代码如下:

swift 复制代码
import Foundation 

class Incrementer {
    @objc dynamic func increment(_ int: Int) -> Int {
        return int + 1
    }
}

let fourPlusOne = Incrementer().increment(4)

执行 swiftc -emit-sil main.swift > sil.txt 指令得到下面的中间代码:

less 复制代码
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):

  // ...
  // 1
  %9 = objc_method %6 : $Incrementer, #Incrementer.increment!foreign : (Incrementer) -> (Int) -> Int, $@convention(objc_method) (Int, Incrementer) -> Int // user: %10
  %10 = apply %9(%8, %6) : $@convention(objc_method) (Int, Incrementer) -> Int // user: %12
  // ... 
}

// Incrementer.increment(_:)
sil hidden @$s4main11IncrementerC9incrementyS2iF : $@convention(method) (Int, @guaranteed Incrementer) -> Int {  
  // 2
  // ... 
}

// @objc Incrementer.increment(_:)
sil private [thunk] @$s4main11IncrementerC9incrementyS2iFTo : $@convention(objc_method) (Int, Incrementer) -> Int {
bb0(%0 : $Int, %1 : $Incrementer):
  // ... 
  // 3 
  %3 = function_ref @$s4main11IncrementerC9incrementyS2iF : $@convention(method) (Int, @guaranteed Incrementer) -> Int // user: %4
  %4 = apply %3(%0, %1) : $@convention(method) (Int, @guaranteed Incrementer) -> Int // user: %6
  strong_release %1 : $Incrementer                // id: %5
  return %4 : $Int                                // id: %6
}

// 4 
sil_vtable Incrementer {
  #Incrementer.init!allocator: (Incrementer.Type) -> () -> Incrementer : @$s4main11IncrementerCACycfC // Incrementer.__allocating_init()
  #Incrementer.deinit!deallocator: @$s4main11IncrementerCfD // Incrementer.__deallocating_deinit
}

1、这里,Incrementer 上的 objc_method 在我们的 main 函数中被调用。请注意 #Incrementer.increment!foreign 表示该方法正在使用 Swift 之外的内容。

2、当你使用 Swift 实现 @objc 方法时,该方法的 Swift 风格和 Objective-C 风格都会在 SIL 中发出。这个经过编辑的 SIL 代码与我们之前查看的静态分派逻辑相同(因为我们将调用它!)。

3、这里,在 @objc Incrementer.increment(_:) 中,[thunk] 让 Objective-C 运行时静态分派到该方法的 Swift 实现。

4、标记为动态的方法不会出现在 v_table 中,因为它不使用常规表派发来解析方法调用。

消息派发是一把双刃剑,使用不当会带来很大的性能消耗,但它绝对在正确的用例中大放异彩。以 Realm 为例,在 iOS 上使用 Realm,你需要将数据库对象的属性标记为 @objc dynamic 。这是为了将它们暴露给 Objective-C 运行时,启用属性上的消息派发,从而启用键值观察!

如何判断当前是哪种派发方式

  • 结构体方法,显然是静态的。
  • 类方法,可以设为静态,但前提是编译器可以在运行时知道类型。
  • open 修饰的类方法,这可能必须使用表派发来查找其他模块中的类。
  • 协议声明的方法,见证表派发。
  • objc dynamic 修饰的方法,消息派发。
相关推荐
光影少年2 天前
webpack打包优化
webpack·掘金·金石计划·前端工程化
光影少年3 天前
Typescript工具类型
前端·typescript·掘金·金石计划
光影少年9 天前
Promise状态和方法都有哪些,以及实现原理
javascript·promise·掘金·金石计划
光影少年9 天前
next.js和nuxt与普通csr区别
nuxt.js·掘金·金石计划·next.js
光影少年9 天前
js异步解决方案以及实现原理
前端·javascript·掘金·金石计划
光影少年13 天前
前端上传切片优化以及实现
前端·javascript·掘金·金石计划
ZTStory16 天前
JS 处理生僻字字符 sm4 加密后 Java 解密字符乱码问题
javascript·掘金·金石计划
光影少年16 天前
webpack打包优化都有哪些
前端·webpack·掘金·金石计划
冯志浩17 天前
Harmony Next - 手势的使用(二)
harmonyos·掘金·金石计划
冯志浩18 天前
Harmony Next - 手势的使用(一)
harmonyos·掘金·金石计划