引言:一个危险的实验
想象一下,你正在调试一个复杂的 iOS 应用,想要在不修改源码的情况下监控所有 UIViewController 的 viewDidAppear 调用;还有如果要支持热修复,该如何?你可能会想到使用 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] 时,运行时:
- 根据对象的类查找方法列表
- 找到对应 selector 的实现(IMP)
- 执行该实现
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)")
}
}
}
总结
关键要点
-
Swift 有三种派发方式:
- 直接派发:最快,用于
final、结构体等 - 表派发:默认,通过虚函数表
- 消息派发:最慢,但支持运行时特性
- 直接派发:最快,用于
-
@objcvsdynamic:@objc:让 Swift 方法对 OC 可见,但 Swift 内仍用表派发dynamic:强制使用消息派发@objc dynamic:OC 可见 + 消息派发
-
Swizzling 的真相:
- 只能交换消息派发的方法
- 交换表派发方法会导致 Swift 和 OC 行为不一致
- 这是很多 Swizzling bug 的根源
-
性能影响:
- 消息派发比直接派发慢 4-5 倍
- 避免在性能关键路径使用
dynamic - 合理使用
final优化性能
哲学思考
Swift 的派发机制体现了语言设计的平衡艺术:
- 安全 vs 灵活:表派发保证安全,消息派发提供灵活
- 性能 vs 功能:直接派发优化性能,动态派发启用高级功能
- Swift vs Objective-C:两种运行时模型的巧妙融合
理解这些机制,你就能:
- 写出更高效的 Swift 代码
- 安全地使用运行时特性
- 避免诡异的 Swizzling bug
- 更好地理解 Swift 的设计哲学
记住:强大的能力伴随着巨大的责任。动态派发给了你 hook 系统方法的能力,但也可能带来难以调试的问题。使用时务必谨慎!