在 Swift 中,@objc 和 dynamic 都与 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. 核心区别对照表
| 特性 | @objc | dynamic |
|---|---|---|
| 主要目的 | 暴露给 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 追求极致的性能。
- 静态派发:编译器直接把函数地址写死,速度最快,支持内联。
@objc但非dynamic:Swift 内部调用时走 V-Table(类似于 C++ 虚函数),只有从 OC 调用时才走消息转发。@objc dynamic:彻底放弃优化,所有调用(即使是 Swift 内部)都走消息转发。
警告:在大型项目中,滥用
dynamic会导致启动时间(Pre-main time)增加,因为系统需要花费更多时间在运行时构建和修整庞大的方法列表。
💡 总结建议
- 如果你只是想把 Swift 方法给
UIButton的Selector用,只写@objc。 - 如果你需要实现 KVO 观察或者使用
Method Swizzling这种黑魔法,必须写@objc dynamic。