Method Swizzling lets you replace a method implementation at runtime without touching the original source code — a technique that powers debugging tools, analytics, and AOP patterns across the iOS ecosystem. Then one day I saw a colleague using Runtime to dynamically add methods, solving a problem where “some methods are only needed in specific scenarios.” That’s when I realized how practical this stuff really is. Method Swizzling, often called “black magic,” is even more powerful. Let’s break it down from principles to real-world code.
The Underlying Structure of Methods in the Runtime
In the Objective-C Runtime, a method is represented by three components: method_name (a selector, or SEL), method_types (a type encoding string), and method_imp (a function pointer, or IMP). These are stored together in the objc_method structure.
A SEL is a method selector — essentially a hashed key derived from the method name, which uniquely identifies the method within the runtime. An IMP is a function pointer that points to the actual implementation address of the method.
Every class maintains a “method dispatch table.” Each entry in this table is a Method that maps a SEL to an IMP. The core principle of method swizzling is to modify the mapping in this table, changing the correspondence between SEL and IMP.
Dynamic Method Addition: When and How
Objective-C’s method calls are based on message sending. When an object receives a message it can’t respond to, the Runtime first calls +resolveInstanceMethod: or +resolveClassMethod:, giving the class a chance to “dynamically add a method implementation.”
#import "Phone.h"
#import <objc/runtime.h>
@implementation Phone
void sayHello(id self, SEL _cmd) {
NSLog(@"sayHello");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"sayHello"]) {
class_addMethod([self class], sel, (IMP)sayHello, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
When using class_addMethod, the fourth parameter types is the method type encoding. "v@:" means: void return, first parameter is id (self), second is SEL (_cmd).
When to use dynamic method addition? The most common scenario is “lazy loading methods.” If a class has many optional methods, loading them all into memory wastes resources. You can add them dynamically when they’re actually needed. Another scenario is dictionary-to-model conversion — dynamically adding setters/getters when properties don’t exist.
The Core Implementation of Method Swizzling
The most common API for swapping is method_exchangeImplementations. There are two golden rules: do it in +load, and wrap it in dispatch_once.
#import "UIViewController+Logging.h"
#import <objc/runtime.h>
@implementation UIViewController (Logging)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(viewDidAppear:));
Method swizzled = class_getInstanceMethod(self, @selector(log_viewDidAppear:));
method_exchangeImplementations(original, swizzled);
});
}
- (void)log_viewDidAppear:(BOOL)animated {
// Call the original method (it's already swizzled, so this actually calls the original viewDidAppear:)
[self log_viewDidAppear:animated];
// Additional tracking logic
NSLog(@"View appeared: %@", NSStringFromClass([self class]));
}
@end
Why use +load? +load is called when the class is loaded into memory — early enough and without manual invocation. +initialize is called only when the class first receives a message. If you swizzle in +initialize, it might be called multiple times if a subclass doesn’t implement it, making the timing unreliable.
A subtle trap: Be careful when calling _cmd. After swizzling, the implementation of log_viewDidAppear: has been replaced with the original viewDidAppear: implementation. So calling log_viewDidAppear: inside this method doesn’t cause recursion — it calls the original implementation.
Advanced Practice: A Safer Swizzling Approach
Using method_exchangeImplementations directly has a hidden risk: if the current class doesn’t implement the method you’re trying to swap, the swap affects the superclass, which can cause unexpected crashes.
A safer approach is to use class_addMethod first:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzled = class_getInstanceMethod(self, @selector(log_viewWillAppear:));
// Try to add the original method using the new method's IMP
BOOL didAddMethod = class_addMethod(self,
@selector(viewWillAppear:),
method_getImplementation(swizzled),
method_getTypeEncoding(swizzled));
if (didAddMethod) {
// Added successfully — the current class didn't implement viewWillAppear:
// Point the new method to the original IMP
class_replaceMethod(self,
@selector(log_viewWillAppear:),
method_getImplementation(original),
method_getTypeEncoding(original));
} else {
// Already existed — just swap
method_exchangeImplementations(original, swizzled);
}
});
}
This approach ensures that if the current class doesn’t implement viewWillAppear:, the superclass’s implementation won’t be swapped — preventing side effects on the superclass and other subclasses.
Practical Example 1: UIButton Anti-Spam
Use Method Swizzling to intercept UIControl‘s sendAction:to:forEvent: method and check the time interval in the swizzled version.
@implementation UIControl (Limit)
+ (void)load {
Method original = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method swizzled = class_getInstanceMethod(self, @selector(limit_sendAction:to:forEvent:));
method_exchangeImplementations(original, swizzled);
}
- (void)limit_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if (self.ignoreEvent) {
NSLog(@"Button click intercepted");
return;
}
if (self.acceptEventInterval > 0) {
self.ignoreEvent = YES;
[self performSelector:@selector(resetIgnoreState) withObject:nil afterDelay:self.acceptEventInterval];
}
[self limit_sendAction:action to:target forEvent:event];
}
@end
Practical Example 2: Safe Array Indexing
NSArray is a class cluster — you can’t swizzle NSArray directly. You need to swizzle its actual implementation class, __NSArrayI.
@implementation NSArray (CrashHandle)
+ (void)load {
Method original = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swizzled = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safe_objectAtIndex:));
method_exchangeImplementations(original, swizzled);
}
- (id)safe_objectAtIndex:(NSUInteger)index {
if (self.count - 1 < index) {
@try {
return [self safe_objectAtIndex:index];
} @catch (NSException *exception) {
// Report crash info online
return nil;
}
}
return [self safe_objectAtIndex:index];
}
@end
Important Considerations
- Always call the original method after swizzling: Unless you’re absolutely certain it’s safe not to, you’re likely breaking the system’s underlying logic.
- Add prefixes to category method names: Avoid name conflicts with system or other library methods.
- Avoid heavy operations in
+load:+loadruns beforemain, so heavy work will slow down launch time. Also, don’t initialize objects here because other classes might not be fully initialized yet. - Understand the call order: If both a category and the main class implement the same method, the category will override the main class — consistent with the Runtime message forwarding mechanism.
Method Swizzling is a double-edged sword. Used well, it’s black magic. Used poorly, it’s a ticking time bomb. The core rule is simple: know exactly what you’re doing, and write it with deep respect for the runtime.
