Changing a method behavior at runtime sounds dangerous, but it is exactly what Method Swizzling does — and it is one of the most powerful debugging and extension tools in the Objective-C toolkit. Later, when I discovered Method Swizzling could swap two methods’ implementations at runtime, I realized that methods are actually dynamic at the runtime level. This technique is often called “black magic” — used well, it solves many tricky problems; used poorly, it’s a ticking time bomb.
The Underlying Principle: The Method Struct Is the Entry Point
In the runtime source code, Method is a struct containing three core elements: SEL (method name), IMP (function pointer), and the Method struct itself. The essence of dynamically modifying method implementations is changing the IMP pointer inside the Method struct.
Code 1: method_exchangeImplementations
// method_exchangeImplementations simplified logic
void method_exchangeImplementations(Method m1, Method m2) {
IMP imp1 = m1->method_imp;
IMP imp2 = m2->method_imp;
m1->method_imp = imp2;
m2->method_imp = imp1;
}
// Practical usage: Swap viewWillAppear: with a custom log_viewWillAppear:
@implementation UIViewController (Logging)
+ (void)load {
Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzled = class_getInstanceMethod(self, @selector(log_viewWillAppear:));
method_exchangeImplementations(original, swizzled);
}
- (void)log_viewWillAppear:(BOOL)animated {
// This actually calls the original viewWillAppear: (swapped!)
[self log_viewWillAppear:animated];
NSLog(@"Page appeared: %@", self);
}
@end
// Subtle detail: when you call log_viewWillAppear:, the selector is
// log_viewWillAppear: but its IMP is the original viewWillAppear:.
// So [self log_viewWillAppear:animated] executes the original logic
// and doesn't recurse.
Code 2: method_setImplementation — Precise Replacement
// method_setImplementation replaces one method's IMP and returns the old one.
// It gives you finer control than method_exchangeImplementations.
// PRACTICAL EXAMPLE 1: Add timing to NSUserDefaults synchronize
static IMP (*original_synchronize)(id, SEL);
static IMP new_synchronize(id self, SEL _cmd) {
NSDate *start = [NSDate date];
IMP result = original_synchronize(self, _cmd);
NSLog(@"synchronize took: %f ms",
[[NSDate date] timeIntervalSinceDate:start] * 1000);
return result;
}
+ (void)load {
Method m = class_getInstanceMethod([NSUserDefaults class],
@selector(synchronize));
original_synchronize = (IMP (*)(id, SEL))method_setImplementation(
m, (IMP)new_synchronize);
}
// PRACTICAL EXAMPLE 2: Network request logging interceptor
static IMP (*original_performSelector)(id, SEL, SEL);
static id new_performSelector(id self, SEL _cmd, SEL selector) {
NSLog(@"[PERFORM] %@ -%@", NSStringFromClass([self class]),
NSStringFromSelector(selector));
NSDate *start = [NSDate date];
id result = original_performSelector(self, _cmd, selector);
NSLog(@"[RESULT] %@ finished in %.3fs",
NSStringFromSelector(selector),
[[NSDate date] timeIntervalSinceDate:start]);
return result;
}
+ (void)load {
Method m = class_getInstanceMethod([NSObject class],
@selector(performSelector:));
original_performSelector = (IMP (*)(id, SEL, SEL))method_setImplementation(
m, (IMP)new_performSelector);
}
// Every performSelector: call now logs the method name and execution time.
// Saved IMP pointer → explicit call path, no selector confusion.
Code 3: class_replaceMethod — Safe Fallback
// class_replaceMethod: if method exists, replace IMP; if not, add it.
// It's the safest option when you're unsure whether the method exists.
@interface UIViewController (Analytics)
- (void)trackViewDidAppear;
@end
@implementation UIViewController (Analytics)
+ (void)load {
SEL sel = @selector(trackViewDidAppear);
IMP imp = imp_implementationWithBlock(^(UIViewController *vc) {
NSLog(@"[Analytics] %@ appeared at %@",
NSStringFromClass([vc class]),
[NSDate date]);
});
// If the method exists → replace its IMP
// If not → add it (like class_addMethod)
class_replaceMethod(self, sel, imp, "v@:");
}
- (void)trackViewDidAppear {
// If we replaced an existing method, this calls the new IMP
// If we added a new method, this is the implementation we just added
}
@end
// class_replaceMethod is best when:
// 1. You're not sure if the current class implements the method
// 2. You want a single API that handles both add and replace
// 3. You don't want to call class_addMethod + class_getInstanceMethod separately
Comparing the Three Approaches
method_exchangeImplementations is the most intuitive, but after swapping, to call the original implementation you must call the new selector — which can be confusing. Best for simple swaps.
method_setImplementation gives you the most precise control. You save the original IMP and call it through a function pointer, avoiding the selector-IMP confusion. Best when you need to preserve the original implementation.
class_replaceMethod is the safest — it handles both replacement and addition. Best when you’re unsure if the method exists.
Important Considerations
• Method signatures must match. If the two methods have different parameter types or return types, calling them after swapping will crash due to mismatched stack layouts.
• Swizzling is global. Modifying a method implementation in a class affects all instances of that class. If you only want to affect specific instances, use inheritance or composition instead.
• Multiple swizzlings can interfere. If multiple third-party libraries swizzle the same method, later ones may overwrite earlier ones. Using method_setImplementation with a saved function pointer is often recommended over simply swapping two methods.
Summary
The core of dynamically modifying method implementations at runtime is manipulating the IMP pointer inside the Method struct. The three APIs have different emphases: method_exchangeImplementations is best for swapping two methods; method_setImplementation is best for precise replacement while preserving the original implementation; class_replaceMethod is best when you’re unsure if the method exists.
No matter which one you use, remember: use dispatch_once to ensure it only executes once, execute it as early as possible in +load, ensure method signatures match, and test thoroughly.
A Real Project Scenario: Timing a Legacy Quote Engine
I have used runtime replacement as a temporary diagnostic tool when a mature pricing component was too widely used to wrap at every call site. The reproducible example contains a legacy DemoQuoteEngine whose quoteForUnits: method is called by existing code. During controlled startup, the program replaces that method with a tracing implementation, measures the duration, calls the saved original IMP, and returns the same quote.
The test is intentionally outcome-focused. Four units must still produce 50.00 with tracing enabled and with the patch bypassed. This catches the most dangerous class of mistake: instrumentation that changes the business result it was supposed to observe.
Validate the Method Before Replacing Its IMP
An IMP is a C function pointer, so the replacement must follow the exact calling convention and argument layout expected by the Objective-C method. The project obtains both the original method and a category-based replacement method, then compares their type encodings before installation. If either method is missing or the encodings differ, installation stops.
Method original = class_getInstanceMethod(
[DemoQuoteEngine class], @selector(quoteForUnits:));
Method replacement = class_getInstanceMethod(
[DemoQuoteEngine class], @selector(demo_tracedQuoteForUnits:));
if (original == NULL || replacement == NULL) return NO;
if (strcmp(method_getTypeEncoding(original),
method_getTypeEncoding(replacement)) != 0) return NO;
The previous IMP returned by method_setImplementation is stored in a correctly typed function pointer. The wrapper calls it with the original selector. Installing during a known single-threaded startup phase avoids exposing another thread to the tiny interval between replacement and saving the previous pointer.
Idempotence, Bypass, and Scope
The installer uses dispatch_once, because applying the same replacement twice can save the wrapper as the “original” implementation and create recursion. A DEMO_DISABLE_PATCH environment flag provides a startup bypass. In a production app, the equivalent might be a remotely delivered configuration read before installation, but it should not repeatedly swap implementations while requests are running.
Keep the target narrow and prefer classes you own. Replacing framework behavior creates compatibility and review risks, while changing a widely used selector can affect every instance in the process. If dependency injection, subclassing, composition, or an ordinary wrapper solves the problem, those approaches are easier to reason about and test. Runtime replacement is best treated as a carefully bounded compatibility or diagnostics technique.
In my test project, the useful check was not a link on a reference page; it was comparing the method type encodings before installing the patch, saving the returned IMP in the matching function-pointer type, and running the same quote through both paths. That small verification caught a mismatched replacement signature before it could turn into a crash. I keep this kind of runtime change behind a startup flag and remove it once the production issue is understood.
Download and Run the Complete Project
The complete standalone project is available in its dedicated GitHub repository: objc-runtime-method-replacement-demo. It requires macOS and the Xcode Command Line Tools.
git clone https://github.com/2252408699/objc-runtime-method-replacement-demo.git
cd objc-runtime-method-replacement-demo
make run
DEMO_DISABLE_PATCH=1 make run
Both commands should print Business result: 50.00 and Result verification: PASS. Only the first run prints the timing trace. The repository includes the complete engine, safe installer, replacement method, bypass, build instructions, and cleanup command.
