12-1.【OC】【Swift+OC 混编】@objc 和 dynamic 有什么区别?

5 阅读3分钟

在 Swift 中,@objcdynamic 都与 Objective-C 运行时(Runtime)有关,但它们的职责完全不同: @objc 负责“可见性”,而 dynamic 负责“派发方式”

我们可以把 @objc 看作是翻译官,把 dynamic 看作是接线员


1. @objc:解决“能不能看见”的问题

@objc 告诉编译器:将这个 Swift 声明(类、方法、属性等)暴露给 Objective-C 运行时,使其在 OC 代码中可见。

  • 底层行为:编译器会为该成员生成一个“桥接存根”(Thunk),这个存根遵循 OC 的命名规范,并允许 objc_msgSend 识别它。
  • 并不改变派发:仅仅加了 @objc,Swift 内部调用该方法时依然可能使用静态派发(Static Dispatch)或虚函数表派发(V-Table Dispatch)。
  • 自动推断:在早期的 Swift 中,继承自 NSObject 的类成员会自动加上 @objc,但在 Swift 4 之后,为了减小二进制体积,你必须手动显式声明。

2. dynamic:解决“如何调用”的问题

dynamic 告诉编译器:不要使用 Swift 的静态优化,必须通过 消息转发(Message Dispatch) 来调用。

  • 底层行为:它强制方法调用走 objc_msgSend。这意味着它在运行时会去查找方法列表,而不是在编译时固定调用地址。
  • 独立存在性:在纯 Swift 中(不涉及 OC 桥接),dynamic 很少单独使用。
  • 特性依赖:它是实现 KVO(键值观察)Method Swizzling(方法交换) 等动态特性的必要条件。

3. 核心区别对照表

特性@objcdynamic
主要目的暴露给 Objective-C 代码使用。强制使用动态消息转发派发。
派发方式可能是静态、V-Table 或消息派发。必定是消息派发(Message Dispatch)。
对 OC 的依赖需要桥接元数据。依赖 Objective-C 运行时机制。
性能较高(Swift 内部调用可优化)。较低(每次都要动态查找 IMP)。
常见用途Target-Action, 协议桥接。KVO, 运行时替换方法 (Swizzling)。

4. 组合使用的场景

在实际开发中,我们经常看到它们成对出现:@objc dynamic var name: String

  • 为什么要连用?

    因为 dynamic 依赖于 objc_msgSend,而 objc_msgSend 只能识别被暴露给 OC 的成员。所以,如果你声明了 dynamic,通常编译器会要求你必须同时加上 @objc

  • 典型场景:KVO

    如果你想观察一个 Swift 属性的变化,必须这样写:

    Swift

    class User: NSObject {
        @objc dynamic var age: Int = 0 // 必须同时具备可见性和动态派发
    }
    

5. 性能影响:为什么要区分它们?

Swift 追求极致的性能。

  1. 静态派发:编译器直接把函数地址写死,速度最快,支持内联。
  2. @objc 但非 dynamic:Swift 内部调用时走 V-Table(类似于 C++ 虚函数),只有从 OC 调用时才走消息转发。
  3. @objc dynamic:彻底放弃优化,所有调用(即使是 Swift 内部)都走消息转发。

警告:在大型项目中,滥用 dynamic 会导致启动时间(Pre-main time)增加,因为系统需要花费更多时间在运行时构建和修整庞大的方法列表。


💡 总结建议

  • 如果你只是想把 Swift 方法给 UIButtonSelector 用,只写 @objc
  • 如果你需要实现 KVO 观察或者使用 Method Swizzling 这种黑魔法,必须写 @objc dynamic