Method Swizzling — 一种运行时技术,在类中两个方法的实现在执行时互换位置。它允许在不创建子类和不修改源代码的情况下覆盖或扩展系统方法的行为。该技术在Objective-C的iOS开发中应用最广泛,但在Kotlin/Android中通过反射存在类似方法。根据NSHipster Guide by Mattt, 2024,swizzling是Objective-C Runtime中最强大但也最危险的机制之一。
要点
Method Swizzling — 一种运行时技术,在执行时交换两个Objective-C方法的实现。Swizzling之后,调用originalSelector会导致执行swizzledSelector的代码,反之亦然。这得益于Objective-C Runtime的架构,其中每个选择器(SEL)通过dispatch table(一个可以在运行时修改的表)与实现(IMP)相关联。
「swizzling」一词于2000年代初期在Cocoa开发者社区中引入。该技术因以下库而广受欢迎:AFNetworking(UIWebView的swizzling用于跟踪加载)、Aspects(基于swizzling的AOP框架)和FLEX(为检查而swizzle系统方法的调试工具)。如今,大多数iOS应用都隐式使用swizzling — 通过监控和分析库。
Swizzling的一个重要特性 — 全局性:实现的替换发生在类级别,而不是实例级别。如果库swizzle了UIViewController.viewDidLoad方法,这将影响应用中所有的UIViewController实例,包括系统实例。这既是swizzling的力量 — 一行代码改变整个应用的行为 — 也是错误的主要来源。
Objective-C Runtime 在每个类中存储一个dispatch table — 一个字典,其中键是SEL(方法标识符),值是IMP(指向实现函数的指针)。当应用向对象发送消息时,objc_msgSend对该表执行线性搜索。Method Swizzling将一个SEL的IMP替换为另一个SEL的IMP,从而重定向调用。
// 安全method swizzling的实现
@implementation NSObject (SafeSwizzle)
+ (void)swizzleClassMethod:(SEL)original
with:(SEL)swizzled {
Class cls = [self class];
SEL originalSel = original;
SEL swizzledSel = swizzled;
Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
关键函数 — method_exchangeImplementations(Method, Method)。它原子地交换两个Method对象的IMP。调用后,类的dispatch table被改变:访问original时执行swizzled代码,访问swizzled时执行original代码。SafeSwizzle类别将此方法添加到所有NSObject,允许任何类执行swizzling。
安全的swizzling实现要求在swizzled版本内部调用原始实现。否则,方法的原始行为将永久丢失。正确的模式 — 在交换前保存原始IMP并在swizzled方法中调用它:
// 调用原始实现的Swizzling
- (void)swizzled_viewDidLoad {
// 1. 调用原始实现
[self swizzled_viewDidLoad];
// 2. 原始调用后的附加逻辑
NSLog("viewDidLoad已执行,swizzling激活");
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleClassMethod:@selector(viewDidLoad)
with:@selector(swizzled_viewDidLoad)];
});
}
dispatch_once保证swizzling在应用生命周期内只执行一次。对同一方法的重复swizzling将导致无限递归:swizzled方法会调用自身。+load在类加载到runtime时被调用 — 这是swizzling的安全点,它在应用主代码之前执行。
Objective-C类的dispatch table — 一个包含SEL、IMP和返回类型的method_t结构数组。method_exchangeImplementations只是交换该表中的两个IMP指针。重要:swizzling仅在类级别工作,而不是协议级别。如果方法在协议中定义但未实现 — dispatch table不包含swizzling的条目。
Swizzling的开销极小 — 交换dispatch table中的两个IMP指针只需几纳秒。Swizzling后,方法调用不会变慢:objc_msgSend在与swizzling之前相同的O(1)时间内找到IMP。唯一的额外操作 — 交换后首次调用时的method cache检查。根据Apple Performance Team的说法,swizzling不影响应用的性能。
Method Swizzling 用于三种主要场景:监控和分析(跟踪viewDidLoad、viewDidAppear以自动发送事件)、AOP拦截(记录所有方法调用的参数)和热修复(通过JSPatch等库在production中修复错误而无需App Store Review)。
每种场景之所以有效,是因为swizzling被集中应用。分析库在+load中执行一次swizzling,应用中所有的UIViewController开始发送事件。开发者无需在每个控制器中添加代码 — 这减少了重复和错误风险。
在Android上,经典Objective-C意义上的method swizzling是不可能的 — Java/Kotlin通过vtable使用静态调度。然而,存在实现类似效果的机制:用于在运行时替换实现的Java反射,以及用于在构建阶段修改字节码的Gradle Transform API / ASM。
// 通过反射和companion object在Android上实现Swizzling
class Logger {
companion object {
var originalImpl: (() -> Unit)? = null
}
fun log() {
println("原始日志")
}
}
// 通过反射在运行时替换实现
fun swizzleLog() {
val originalMethod = Logger::class.java
.getDeclaredMethod("log")
originalMethod.isAccessible = true
Logger.originalImpl = {
originalMethod.invoke(Logger())
}
// 通过内联函数替换
println("swizzled:日志已拦截")
}
这段代码通过Java反射替换了log()方法的行为:getDeclaredMethod获取私有实现的访问权限,isAccessible禁用访问检查。不是直接调用log(),而是调用一个执行额外逻辑的封装器。然而,Android通过JIT优化热方法 — 反射可能无法在已编译的AOT部分上工作。
更可靠的方法 — 使用ASM库通过Gradle Transform API或AGP(Android Gradle Plugin)进行字节码操作。字节码的修改在编译阶段执行:ASM向类的每个方法添加调用。代码覆盖率工具(JaCoCo)和性能监控工具(Firebase Performance Monitoring)就是这样工作的。
Method Swizzling — 高风险技术。库之间的冲突:如果两个库swizzle相同的方法,执行顺序无法保证。与iOS更新的不兼容:如果Apple在新版iOS中更改签名或删除方法,swizzling会导致崩溃。代码中不可见:swizzling在类实现中不可见,使调试变得困难。
| 风险 | 描述 | 缓解措施 |
|---|---|---|
| 库冲突 | 两个库swizzle viewDidAppear — 一个破坏另一个 | 通过class_getInstanceMethod检查方法是否已被swizzle |
| 递归 | 重复swizzling同一方法导致无限循环 | 始终使用dispatch_once |
| 签名更改 | Apple在新iOS中更改方法签名 — IMP不匹配 | 在所有支持的iOS版本上测试 |
| 不可见性 | Swizzling不显示在Xcode调用堆栈中 | 在代码中记录所有swizzling操作 |
| App Review | Apple拒绝包含未记录swizzling的应用 | 仅使用公共API并记录目的 |
安全swizzling的最佳实践包括:始终调用原始实现,严格在+load中通过dispatch_once执行swizzling,使用前缀命名swizzled方法(例如s_originalMethodName),记录每个swizzling操作及其目的。Aspects库通过在原始方法之前/之后链式执行块来解决冲突问题。
Method swizzling的替代方案由于可预测性和安全性而更适合生产代码。委托和协议(UIApplicationDelegate, UITableViewDelegate)提供明确的扩展点,无需修改运行时。子类化 — 创建UIViewController的子类并重写viewDidAppear — 可预测地工作且没有冲突。
SwiftUI和Combine消除了对swizzling的需求:修饰符(onAppear, onChange)以声明方式添加行为,无需重写方法。在Android上,Jetpack Compose通过效果(LaunchedEffect, SideEffect)和修饰符实现相同的效果。AOP框架(Android的AspectJ,iOS的InterposeKit)通过编译时织入提供安全的替代方案。
根据Apple WWDC 2024,Swift运行时在语言级别不支持method swizzling — @objc dynamic方法只能通过Objective-C Runtime进行swizzle。不使用@objc的Swift应用完全受到保护,免受第三方库的意外swizzling。这使得Swift更安全,但限制了运行时工具化的能力。
SwiftUI修饰符(onAppear, onChange, onReceive)和Jetpack Compose效果(LaunchedEffect, SideEffect, DisposableEffect)完全取代了UI任务的swizzling。它们提供了一种声明式、可预测且可测试的方式来添加横切行为,而无需修改dispatch table。在新项目中,Apple和Google推荐采用这种方法而不是运行时拦截。
常见问题
Method Swizzling在遵守规则的情况下可以用于生产环境:使用dispatch_once确保一次性执行,调用原始实现,在所有iOS版本上测试并进行文档记录。对于简单任务,最好使用委托或子类化。生产中的Swizzling对于监控和分析库来说是合理的。
Method Swizzling — 在dispatch table中替换IMP的具体技术。AOP(面向切面编程) — 是一种范式,其中swizzling可以作为机制之一使用。AOP还包括编译时织入(AspectJ)、基于代理的拦截(Spring AOP)和代码生成。
在objc_msgSend中使用断点跟踪所有消息。在method_exchangeImplementations上添加带有类名条件的符号断点。FLEX工具显示类的哪些方法已被swizzle。对于系统检查,使用显示类dispatch table的lldb脚本。
Swift在语言级别不支持swizzling。Method Swizzling仅对标记为@objc dynamic的方法有效,这些方法通过Objective-C Runtime编译。纯Swift方法(无@objc)使用静态调度,无法被swizzle — 它们的dispatch table无法修改。
Firebase Analytics(对viewDidAppear进行swizzling以自动跟踪屏幕)、Amplitude、Mixpanel、FLEX(UI检查)、OHHTTPStubs(网络请求模拟)、Aspects(AOP框架)。它们都在+load中通过dispatch_once执行swizzling并调用原始实现。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。