理解 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 修饰的方法,消息派发。
相关推荐
冯志浩16 天前
Harmony NEXT:如何给数据库添加自定义分词
harmonyos·掘金·金石计划
中杯可乐多加冰2 个月前
【AI落地应用实战】DAMODEL深度学习平台部署+本地调用ChatGLM-6B解决方案
人工智能·掘金·金石计划
中杯可乐多加冰2 个月前
Amazon Bedrock +Amazon Step Functions实现链式提示(Prompt Chaining)
人工智能·掘金·金石计划
阿李贝斯3 个月前
el-select海量数据渲染-分页解决方案
前端·javascript·掘金·金石计划
宇宙之一粟4 个月前
Error Flows in Go
后端·go·掘金·金石计划
雨绸缪4 个月前
如何在 Eclipse 中调试ABAP程序
后端·掘金·金石计划
中杯可乐多加冰4 个月前
解决方案:昇腾aarch64服务器安装CUDA+GCC+CMake,编译安装Pytorch,华为昇腾HPC服务器深度学习环境安装全流程
人工智能·掘金·金石计划
雨绸缪5 个月前
第 1章 BW 建模工具概念介绍
后端·数据挖掘·掘金·金石计划
宇宙之一粟5 个月前
Python Asyncio 如何工作?从零开始重新创建
后端·python·掘金·金石计划