Cocoaphony

Hijacking With method_exchangeImplementations()

Based on a discussion from StackOverflow.

Sometimes you want to inject some logic into a method you don’t control. The most common sane reason to do this is for debugging or profiling. For instance, you might want to log every time the various NSNotificationCenter methods are called so you can determine if that’s a performance bottleneck. (As I discovered myself, if you have thousands of notification observations in your system, it can be a serious performance problem.)

In most OOP languages your only option would be to subclass the object you want to instrument and then arrange for every instance of that object to be your subclass. In many cases that’s either very difficult or outright impossible, particularly if the object is used internally by a system framework. But Objective-C is highly dynamic, and message dispatching is resolved at runtime. You can modify how it works.

On Mac, there is an NSObject method called poseAsClass: that can achieve this easily. Unfortunately, it’s deprecated in 10.5 and isn’t available at all for Mac 64-bit and iPhone. I want a technique that I can use reliably for all my platforms, and luckily there is a fully supported alternative: method_exchangeImplemenations().

So how do we use it? First, we create a category on our target (NSNotificationCenter):

@interface NSNotificationCenter (RNHijack)
+ (void)hijack;
@end

@implementation NSNotificationCenter (RNHijack)
+ (void)hijackSelector:(SEL)originalSelector withSelector:(SEL)newSelector
{
    Class class = [NSNotificationCenter class];
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method categoryMethod = class_getInstanceMethod(class, newSelector);
    method_exchangeImplementations(originalMethod, categoryMethod);
}

+ (void)hijack
{
    [self hijackSelector:@selector(removeObserver:)
        withSelector:@selector(RNHijack_removeObserver:)];
}

- (void)RNHijack_removeObserver:(id)notificationObserver
{
    NSLog(@"Removing observer: %@", notificationObserver);
    [self RNHijack_removeObserver:notificationObserver];
}

Then, somewhere early in my program, I call [NSNotificationCenter hijack]. Now, every removeObserver: message is logged, no matter who sends it, even inside Cocoa itself. To make sure that I catch everything, I often put this in main.m, before the call to ‘NSApplicationMain()’.

The line to pay particular notice to is the apparent infinite loop in RNHijack_removeObserver: that seems to call itself. Why does this work? Because method_exchangeImplementation() swaps the two implementations. The old removeObserver: now points to my implementation, but conversely RNHijack_removeObserver: points to the old implementation.

It is hard to imagine code that is more confusing and fragile than this, which is why this technique must be used with the utmost care. Personally I can’t imagine using this outside of debugging and profiling. But when you need it, it is an incredibly powerful tool.