Swift 方法派发深度解析:从 Swizzling 到派发机制

引言:一个危险的实验

想象一下,你正在调试一个复杂的 iOS 应用,想要在不修改源码的情况下监控所有 UIViewControllerviewDidAppear 调用;还有如果要支持热修复,该如何?你可能会想到使用 Method Swizzling

swift 复制代码
extension UIViewController {
    @objc dynamic func swizzled_viewDidAppear(_ animated: Bool) {
        print("🎯 [AOP] \(type(of: self)) 显示")
        swizzled_viewDidAppear(animated) // 调用原始实现
    }
    
    static func swizzleViewDidAppear() {
        let original = #selector(viewDidAppear(_:))
        let swizzled = #selector(swizzled_viewDidAppear(_:))
        
        guard let originalMethod = class_getInstanceMethod(self, original),
              let swizzledMethod = class_getInstanceMethod(self, swizzled) else {
            return
        }
        
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
}

看起来完美,对吧?但这里隐藏着一个 Swift 的重要秘密:为什么必须使用 @objc dynamic 如果去掉 dynamic 会发生什么?

Part 1: 为什么 Swizzling 需要动态派发?

1.1 Swizzling 的工作原理

Method Swizzling 本质上是在运行时交换两个方法的实现。它依赖 Objective-C 运行时的消息派发机制:

objc 复制代码
// Objective-C 运行时的工作方式
objc_msgSend(object, selector, ...)

当调用 [object method] 时,运行时:

  1. 根据对象的类查找方法列表
  2. 找到对应 selector 的实现(IMP)
  3. 执行该实现

Swizzling 就是修改了第 2 步的映射关系。

1.2 Swift 与 Objective-C 的冲突

问题在于:Swift 默认不使用消息派发

swift 复制代码
class MyClass {
    func normalMethod() { }     // Swift 默认派发
    @objc func exposedMethod() { }  // 对 OC 可见,但仍不是消息派发
    @objc dynamic func dynamicMethod() { }  // 这才是消息派发
}

如果你尝试 Swizzle 一个非 dynamic 的方法:

swift 复制代码
class TestSwizzle: NSObject {
    @objc func original() { print("Original") }
    @objc func swizzled() { print("Swizzled") }
    
    static func attemptSwizzle() {
        let original = #selector(original)
        let swizzled = #selector(swizzled)
        
        guard let origMethod = class_getInstanceMethod(self, original),
              let swizMethod = class_getInstanceMethod(self, swizzled) else {
            return
        }
        
        print("交换前:")
        print("original IMP: \(method_getImplementation(origMethod))")
        print("swizzled IMP: \(method_getImplementation(swizMethod))")
        
        method_exchangeImplementations(origMethod, swizMethod)
        
        print("交换后:")
        print("original IMP: \(method_getImplementation(origMethod))")
        print("swizzled IMP: \(method_getImplementation(swizMethod))")
        
        let test = TestSwizzle()
        test.original()  // 输出什么?
    }
}

运行结果可能让你困惑:

yaml 复制代码
交换前:
original IMP: 0x0000000102f7fbc0
swizzled IMP: 0x0000000102f7fcc0

交换后:
original IMP: 0x0000000102f7fcc0
swizzled IMP: 0x0000000102f7fbc0

Original   //❓ 调用结果还是 "Original"!

为什么 IMP 发生交换后,但行为没变?

Part 2: Swift 的三种派发方式

2.1 派发方式对比

假设大家已有概念,为了方便快速浏览,我把这些汇总到了一个表格:

特性 直接派发 (Direct Dispatch) 表派发 (Table Dispatch) 消息派发 (Message Dispatch)
Swift 写法 final func struct 的方法 extension 中的方法 private/fileprivate func class func (默认) @objc func (仅 Swift 内) @objc dynamic func @objc dynamic var
调用方式 编译时确定地址,直接跳转 通过在类对象虚函数表查找 Objective-C 运行时 objc_msgSend
性能 ⚡️ 最快 (几乎无开销) 较快 (一次指针查找) 🐌 最慢 (哈希查找+缓存)
灵活性 ❌ 最低 (无法重写) ✅ 中等 (支持继承重写) ✅✅ 最高 (支持运行时修改)
内存占用 无额外开销 每个类一个虚函数表 每个类方法列表 + 缓存
重写支持 ❌ 不支持 ✅ 支持 ✅ 支持
运行时修改 ❌ 不可能 ❌ 不可能 (Swift 5+) ✅ 可能 (Method Swizzling)
KVO 支持 ❌ 不支持 ❌ 不支持 ✅ 支持
典型应用 工具方法、性能关键代码 普通业务逻辑、可继承的类 需要动态特性的代码
二进制影响 最小 中等 最大 (生成 OC 元数据)
调试难度 简单 中等 困难 (调用栈复杂)

2.2 方法派发特别注意点

extension 中的方法特别说明:

extension 中的方法默认是静态派发,不能被子类重写,编译器可以在编译时确定具体实现。 这样设计的原因有下面几点:

  • 明确性: extension表示添加新功能,override表示修改现有功能,两者分离,避免混淆。
  • 安全性:不允许重写 → 保证 extension 方法的稳定性。
  • 模块化:不用担心用户重写了自己模块extension中方法导致异常。
  • 良好实践:使用 extension 分离关注点。
Swift 复制代码
class BaseClass {
    // 进入类的虚函数表
    func original() { print("Base original") }
}

extension BaseClass {
    // 不在虚函数表中!相当于直接是函数地址
    // 编译后的伪代码: 是生成一个全局函数
    // void String_extension_customMethod(String *self) {
    //     // 函数体
    // }
    func extensionMethod() { print("extension method") }
    
    // ❌ 不能在 extension 中重写原类方法
    // override func original() { }  // 编译错误
}

class SubClass: BaseClass {
    // ✅ 可以重写原类方法
    override func original() { print("SubClass original") }
    
    // ❌ 不能重写 extension 中的方法
    // override func extensionMethod() { }  // 编译错误
}

对 Objective-C 类的 extension

Swift 复制代码
// Objective-C 类(如 UIView)
extension UIView {
    // 仍然是直接派发(在 Swift 中调用时)
    func swiftExtensionMethod() { }
    
    // 但通过 @objc 暴露给 Objective-C 时
    @objc func objcExposedMethod() { }  
    // Swift 内:直接派发
    // Objective-C 内:通过桥接,底层是消息派发
}

extension NSObject {
    @objc dynamic func specialMethod() { }
    // 这会强制使用消息派发
    // 可以被重写(因为是消息派发)
    // 但这是特殊情况,利用了 Objective-C 运行时
}

协议扩展extension

Swift 复制代码
protocol Drawable {
    func draw()  // 协议要求
}

extension Drawable {
    func draw() {  // 默认实现
        print("默认绘制")
    }
    // 这是直接派发,但可以通过协议类型动态派发
}

class Circle: Drawable { }

let circle = Circle()
circle.draw()  // 直接派发:调用默认实现

let drawable: Drawable = circle
drawable.draw()  // 协议派发:通过协议见证表PWT(Protocol Witness Table)
// 内存布局
Circle 实例:
┌──────────┐
│ 数据字段  │
├──────────┤
│ PWT 指针 │ → 指向 Circle 的协议见证表
└──────────┘

Circle 的 PWT:
┌──────────┐
│ draw()   │ ← 索引 0
├──────────┤
│ resize() │ ← 索引 1
└──────────┘

2.3 Swift 类的虚函数表

scss 复制代码
对象实例内存布局:
┌───────────────────┐
│    对象头 (16字节)  │ ← 包含指向类对象的指针
├───────────────────┤
│  引用计数 (8字节)   │
├───────────────────┤
│  属性 name (8字节) │
├───────────────────┤
│  属性 age (8字节)  │
└───────────────────┘

类对象内存布局:
┌───────────────────┐
│   类信息 (元数据)   │
├───────────────────┤
│  虚函数表指针      │ → 指向虚函数表数组
├───────────────────┤
│  其他元数据...     │
└───────────────────┘

虚函数表结构:
┌───────────────────┐
│    makeSound()    │ ← 函数指针 [0]
├───────────────────┤
│       eat()       │ ← 函数指针 [1]
├───────────────────┤
│      sleep()      │ ← 函数指针 [2]
└───────────────────┘

虚函数表(V-Table)工作原理:

markdown 复制代码
Dog 类的虚函数表(编译时根据顺序确定索引):
[0]: Dog.makeSound() 地址
[1]: Dog.otherMethod() 地址
...

调用 animal.makeSound():
1. 获取 animal 的虚函数表指针
2. 根据索引得到 makeSound 在表中的地址(编译时确定)
3. 跳转到对应地址执行

2.4 Swift 类方法的派发

类元数据结构:

css 复制代码
┌─────────────────────┐
│    类型描述符        │ ← Metadata header
├─────────────────────┤
│    父类指针          │
├─────────────────────┤
│    实例变量偏移      │
├─────────────────────┤
│  ↓ 实例方法表指针    │ → 指向实例方法的虚函数表
├─────────────────────┤
│  ↓ 类方法表指针      │ → 指向类方法的独立表
├─────────────────────┤
│    协议列表指针      │
├─────────────────────┤
│    泛型信息...       │
└─────────────────────┘

实例方法表(虚函数表):
┌─────────────────────┐
│    instanceMethod1   │ ← 索引 0
├─────────────────────┤
│    instanceMethod2   │ ← 索引 1
└─────────────────────┘

类方法表:
┌─────────────────────┐
│     classMethod1     │ ← 索引 0  
├─────────────────────┤
│     classMethod2     │ ← 索引 1
└─────────────────────┘

派发方式

swift 复制代码
class MyClass {
    
    // 默认的表派发类方法
    class func classMethod() {  // 通过类的元数据进行派发
        print("类方法 - 表派发")
    }
    
    // 直接派发,不能被重写
    static func staticMethod() {  
        print("静态方法 - 直接派发")
    }
    
    // final class func 等价于 static func
    final class func alsoCannotOverride() { }
    
    @objc dynamic class func dynamicClassMethod() {
        print("类方法 - 消息派发")
    }
}

class AppConfig {
    // 存储在全局数据段
    static let appName = "MyApp"        // __TEXT 段(只读)
    static var launchCount = 0          // __DATA 段(读写)
    static let shared = AppConfig()     // 引用存储在全局,对象在堆上
    
    // 惰性静态属性
    static lazy var heavyResource = createHeavyResource()
}

/*
内存位置:
- appName: 编译时常量 → 代码段
- launchCount: 全局变量 → 数据段  
- shared: 引用在数据段,对象在堆上
- heavyResource: 第一次访问时初始化
*/

Part 3: 混合派发的危险实验

3.1 当 Swift 遇到 Swizzling

让我们看一个更完整的例子:

swift 复制代码
class MixedClass {
    // 情况1:纯 Swift
    func swiftMethod() { print("Swift Method") }
    
    // 情况2:暴露给 OC,但 Swift 内使用表派发
    @objc func exposedMethod() { print("Exposed Method") }
    
    // 情况3:完全动态
    @objc dynamic func dynamicMethod() { print("Dynamic Method") }
}

// 尝试 Swizzle
extension MixedClass {
    @objc dynamic func swizzled_swiftMethod() {
        print("Swizzled Swift")
        swizzled_swiftMethod()
    }
    
    @objc func swizzled_exposedMethod() {
        print("Swizzled Exposed")
        swizzled_exposedMethod()
    }
    
    @objc dynamic func swizzled_dynamicMethod() {
        print("Swizzled Dynamic")
        swizzled_dynamicMethod()
    }
    
    static func testAll() {
        let instance = MixedClass()
        
        print("=== 原始调用 ===")
        instance.swiftMethod()      // Swift Method
        instance.exposedMethod()    // Exposed Method
        instance.dynamicMethod()    // Dynamic Method
        
        // 尝试 Swizzle swiftMethod(缺少 dynamic)
        if let orig = class_getInstanceMethod(self, #selector(swiftMethod)),
           let swiz = class_getInstanceMethod(self, #selector(swizzled_swiftMethod)) {
            method_exchangeImplementations(orig, swiz)
        }
        
        // 尝试 Swizzle exposedMethod(只有 @objc)
        if let orig = class_getInstanceMethod(self, #selector(exposedMethod)),
           let swiz = class_getInstanceMethod(self, #selector(swizzled_exposedMethod)) {
            method_exchangeImplementations(orig, swiz)
        }
        
        // Swizzle dynamicMethod(正确方式)
        if let orig = class_getInstanceMethod(self, #selector(dynamicMethod)),
           let swiz = class_getInstanceMethod(self, #selector(swizzled_dynamicMethod)) {
            method_exchangeImplementations(orig, swiz)
        }
        
        print("\n=== Swizzle 后调用 ===")
        instance.swiftMethod()      // 还是 Swift Method ❌
        instance.exposedMethod()    // 还是 Exposed Method ❌  
        instance.dynamicMethod()    // Swizzled Dynamic ✅
        
        print("\n=== 通过 OC 运行时调用 ===")
        // 通过 performSelector 调用
        instance.perform(#selector(swiftMethod))      // 可能崩溃 💥
        instance.perform(#selector(exposedMethod))    // Swizzled Exposed ✅
        instance.perform(#selector(dynamicMethod))    // Swizzled Dynamic ✅
    }
}

3.2 为什么会这样?

内存布局解释:

当 Swift 编译一个类时:

  • 纯 Swift 方法 → 放入虚函数表
  • @objc 方法 → 生成桥接方法,同时放入虚函数表和 OC 方法列表(在Swift中调用未交换,OC中调用时已交换)
  • @objc dynamic 方法 → 直接放入 OC 方法列表

Swizzling 只影响 OC 方法列表,不影响虚函数表!

Part 4: 属性的 @objc dynamic

4.1 Swift中使用KVO

swift 复制代码
class Observable: NSObject {
    // 普通属性,不支持 KVO
    var name: String = ""
    
    // @objc dynamic 属性,支持 KVO
    @objc dynamic var age: Int = 0
    
    // 只有 @objc,不支持 KVO
    @objc var height: Double = 0.0
}

let obj = Observable()

// 尝试观察
// 运行时错误**Fatal error: Could not extract a String from KeyPath Swift.ReferenceWritableKeyPath<XXX.Observable, Swift.String>**
obj.observe(\.name, options: .new) { _, change in
    print("name changed: \(change.newValue ?? "nil")")
}  

obj.observe(\.age, options: .new) { _, change in
    print("age changed: \(change.newValue ?? 0)")
}  // ✅ 正常工作

obj.observe(\.height, options: .new) { _, change in
    print("height changed: \(change.newValue ?? 0)")
}  // 无法观察到变化

4.2 属性访问的派发方式

swift 复制代码
class PropertyTest {
    // 直接派发(编译时展开),会被内联
    var directProperty: Int {
        get { return _storage }
        set { _storage = newValue }
    }
    
    // 表派发(通过方法)
    var tableProperty: Int {
        get {
            print("getter 调用")
            return _storage
        }
        set {
            print("setter 调用")
            _storage = newValue
        }
    }
    
    // 消息派发(支持 KVO)
    @objc dynamic var messageProperty: Int {
        get { return _storage }
        set { _storage = newValue }
    }
    
    private var _storage: Int = 0
}

// @objc dynamic 属性会生成:
// - (NSInteger)messageProperty;
// - (void)setMessageProperty:(NSInteger)value;
// 这些是真正的 Objective-C 方法

4.3 属性观察器的有趣现象

swift 复制代码
class Observed: NSObject {
    @objc dynamic var value: Int = 0 {
        didSet {
            print("value 从 \(oldValue) 变为 \(value)")
        }
    }
    
    // 测试 KVO 和 didSet 的交互
    func test() {
        self.value = 10  // 触发 didSet
        
        // 通过 KVC 设置
        self.setValue(20, forKey: "value")  // 也会触发 didSet ✅
    }
}

// 为什么能工作?
// @objc dynamic 属性生成的 setter 会:
// 1. 调用 willChangeValueForKey
// 2. 设置新值
// 3. 调用 didSet(Swift 注入的代码)
// 4. 调用 didChangeValueForKey(触发 KVO)

Part 5: 派发方式的确定规则

5.1 决策树

swift 复制代码
// Swift 编译器决定派发方式的逻辑:
func determineDispatch(for method: Method) -> DispatchType {
    if method.isFinal || type.isFinal || type.isStruct {
        return .direct      // 1. final 或 struct → 直接派发
    }
    
    if method.isDynamic {
        return .message     // 2. dynamic → 消息派发
    }
    
    if method.isObjC {
        // @objc 但不 dynamic:桥接方法
        return .table       // 3. 在 Swift 内使用表派发
    }
    
    if method.isInExtension && !type.isObjCClass {
        return .direct      // 4. 非 OC 类的扩展 → 直接派发
    }
    
    return .table           // 5. 默认 → 表派发
}

5.2 特殊情况

swift 复制代码
// 1. 协议要求
protocol MyProtocol {
    func requiredMethod()   // 表派发(通过协议见证表)
}

// 2. 泛型约束
func genericFunc<T: MyProtocol>(_ obj: T) {
    obj.requiredMethod()    // 静态派发(编译时特化)
}

// 3. @_dynamicReplacement
class Replaceable {
    dynamic func original() { print("Original") }
}

extension Replaceable {
    @_dynamicReplacement(for: original)
    func replacement() { print("Replacement") }
}
// Swift 5 引入的官方 "Swizzling"

Part 6: 性能影响与优化

优化建议

swift 复制代码
// ❌ 避免在性能关键路径使用 dynamic
class Cache {
    @objc dynamic var data: [String: Any] = [:]  // 每次访问都有消息派发开销
    
    func expensiveOperation() {
        for _ in 0..<10000 {
            _ = data["key"]  // 慢!
        }
    }
}

// ✅ 优化方案
class OptimizedCache {
    private var _data: [String: Any] = [:]
    
    var data: [String: Any] {
        get { return _data }
        set { _data = newValue }
    }
    
    @objc dynamic var observableData: [String: Any] {
        get { return _data }
        set { _data = newValue }
    }
    
    func expensiveOperation() {
        let localData = data  // 一次读取
        for _ in 0..<10000 {
            _ = localData["key"]  // 快!
        }
    }
}

Part 7: 实际应用指南

7.1 何时使用何种派发?

swift 复制代码
// 指南:
class MyClass {
    // ✅ 使用直接派发:
    final func utilityMethod() { }  // 工具方法,不重写
    private func helper() { }       // 私有方法
    
    // ✅ 使用表派发(默认):
    func businessLogic() { }        // 业务逻辑,可能被重写
    open func publicAPI() { }       // 公开 API
    
    // ⚠️ 谨慎使用消息派发:
    @objc dynamic func kvoProperty() { }  // 需要 KVO
    @objc dynamic func swizzleMe() { }    // 需要 Method Swizzling
    
    // ❌ 避免混用:
    @objc func confusingMethod() { }  // 既不是鱼也不是熊掌
    // 在 Swift 中是表派发,在 OC 中是消息派发
    // 可能导致不一致的行为
}

7.2 安全 Swizzling 的最佳实践

swift 复制代码
class SafeSwizzler {
    /// 安全的 Method Swizzling
    static func swizzle(_ type: AnyClass,
                       original: Selector,
                       swizzled: Selector,
                       isClassMethod: Bool = false) throws {
        
        // 1. 获取方法
        let getMethod = isClassMethod ? class_getClassMethod : class_getInstanceMethod
        guard let originalMethod = getMethod(type, original),
              let swizzledMethod = getMethod(type, swizzled) else {
            throw SwizzleError.methodNotFound
        }
        
        // 2. 检查是否已经是消息派发
        let originalEncoding = method_getTypeEncoding(originalMethod)
        if originalEncoding == nil {
            throw SwizzleError.notMessageDispatch
        }
        
        // 3. 检查是否已经 Swizzled
        if alreadySwizzled {
            throw SwizzleError.alreadySwizzled
        }
        
        // 4. 执行交换
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
    
    enum SwizzleError: Error {
        case methodNotFound
        case notMessageDispatch
        case alreadySwizzled
    }
}

// 使用
extension UIViewController {
    @objc dynamic func safe_viewDidLoad() {
        print("Safe tracking")
        safe_viewDidLoad()
    }
    
    static func enableSafeTracking() {
        do {
            try SafeSwizzler.swizzle(
                UIViewController.self,
                original: #selector(viewDidLoad),
                swizzled: #selector(safe_viewDidLoad)
            )
            print("✅ Safe swizzling 成功")
        } catch {
            print("❌ Swizzling 失败: \(error)")
        }
    }
}

总结

关键要点

  1. Swift 有三种派发方式

    • 直接派发:最快,用于 final、结构体等
    • 表派发:默认,通过虚函数表
    • 消息派发:最慢,但支持运行时特性
  2. @objc vs dynamic

    • @objc:让 Swift 方法对 OC 可见,但 Swift 内仍用表派发
    • dynamic:强制使用消息派发
    • @objc dynamic:OC 可见 + 消息派发
  3. Swizzling 的真相

    • 只能交换消息派发的方法
    • 交换表派发方法会导致 Swift 和 OC 行为不一致
    • 这是很多 Swizzling bug 的根源
  4. 性能影响

    • 消息派发比直接派发慢 4-5 倍
    • 避免在性能关键路径使用 dynamic
    • 合理使用 final 优化性能

哲学思考

Swift 的派发机制体现了语言设计的平衡艺术:

  • 安全 vs 灵活:表派发保证安全,消息派发提供灵活
  • 性能 vs 功能:直接派发优化性能,动态派发启用高级功能
  • Swift vs Objective-C:两种运行时模型的巧妙融合

理解这些机制,你就能:

  • 写出更高效的 Swift 代码
  • 安全地使用运行时特性
  • 避免诡异的 Swizzling bug
  • 更好地理解 Swift 的设计哲学

记住:强大的能力伴随着巨大的责任。动态派发给了你 hook 系统方法的能力,但也可能带来难以调试的问题。使用时务必谨慎!

相关推荐
cn_mengbei1 天前
从零到一:基于Qt on HarmonyOS的鸿蒙PC原生应用开发实战与性能优化指南
qt·性能优化·harmonyos
DemonAvenger1 天前
Redis慢查询分析与优化:性能瓶颈排查实战指南
数据库·redis·性能优化
triumph_passion1 天前
Zustand 从入门到精通:我的工程实践笔记
前端·性能优化
shughui1 天前
JMter(六):jmete变量提取常用方式
jmeter·性能优化
dyxal1 天前
Excel情感标注工具性能优化实战:从卡顿到流畅的蜕变
网络·性能优化·excel
小宇的天下1 天前
Calibre 3Dstack --每日一个命令days8【connected】(3-8)
运维·服务器·性能优化
山峰哥1 天前
数据库工程与SQL调优实战:从原理到案例的深度解析
java·数据库·sql·oracle·性能优化·编辑器
IT大白1 天前
性能优化篇
数据库·性能优化
cn_mengbei1 天前
鸿蒙原生PC应用开发实战:从零搭建到性能优化,掌握ArkTS与DevEco Studio高效开发技巧
华为·性能优化·harmonyos