消息派发是 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
修饰的方法,消息派发。