Objectiv-C is very dynamic language that allows for some neat tricks. One of them is to exchange one method with another. While that doesn't sound useful at first, it allows you to "extend" or "augment" an existing method.
In a project I was working on, I was experiencing strange occasional UI "hangs". The UI wasn't responding any more for a few seconds every now and then. I had the suspicion that this was related to writing the NSUserDefaults (i. e. calling [[NSUserDefaults standardUserDefaults] synchronize];
). So I wanted to measure how long the synchronize
takes. I could have added NSLog
s everywhere I call synchronize
, but iOS also calls that method from time to time (for example when your apps enter background because you pressed the Home button). How to catch these as well?
One solution is method swizzling. This is name for replacing a method with another. In this case, I wanted to wrap the existing synchronize
so that every time it was called it would also print how long it was running. And here's how to do this:
First, we need to create a category so we can extend the existing NSUserDefaults
class:
NSUserDefaults+Timing.h:
#import <Foundation/Foundation.h>
@interface NSUserDefaults(Timing)
@end
The interface is empty as we're not adding any user-visible methods.
Step 2: Create the wrapper methodNSUserDefaults+Timing.m:
#import "NSUserDefaults+Timing.h"
#import <objc/runtime.h> // Needed for method swizzling
@implementation NSUserDefaults(Timing)
- (BOOL)swizzled_synchronize
{
NSDate *started;
BOOL returnValue;
started = [NSDate date];
returnValue = [self swizzled_synchronize];
NSLog(@"Writing user defaults took %f seconds.", [[NSDate date] timeIntervalSinceDate:started]);
return returnValue;
}
@end
Now this looks like it's an endlessly recursing method as it calls itself over and over again. This is most confusing part of method swizzling and will be explained below.
Step 3: Swizzle diz!Now that there's a wrapper method we actually need to replace the existing synchronize
with our wrapper. This is done by adding the following method to our implementation:
+ (void)load
{
Method original, swizzled;
original = class_getInstanceMethod(self, @selector(synchronize));
swizzled = class_getInstanceMethod(self, @selector(swizzled_synchronize));
method_exchangeImplementations(original, swizzled);
}
Step 4: Profit! Let's discuss what we've done.
I'm now going to explain what the code is doing, step by step:
- When the app is starting, our
+ (void)load
is called very early in the app life-cycle. In fact, it's run even beforemain
. This is a special feature of Objective-C. For details, see the documentation of+(void)load
. - There are a few C function with which we can interface with the Object-C runtime. We're using
class_getInstanceMethod
to get the methodssynchronize
andswizzled_synchronize
- And then we're swapping their implementations with
method_exchangeImplementations
. This means that when someone now calls thesynchronize
method, the code fromswizzled_synchronize
is actually executed and vice versa.
Alright, so what's happening now when synchronize
is called?
- Instead of the old code, the method that we implemented as
swizzled_synchronize
is called. - In this method, it looks like we're calling the same method again, causing and endless recursion. But by the time this line is reached the two method have been swapped. So when we call
swizzled_synchronize
we're actually calling the original method.
Now let's use this for something more fun: the following code will draw borders around all UIView
s. Views that were set up "manually" through initWithFrame:
get a red border, views that were unarchived (for example as part of XIBs) get a blue border.
Using this revealed something I didn't know: the status bar at the top of an iOS app is drawn by the app itself. Proof:
UIView+Border.h:
#import <Foundation/Foundation.h>
@interface UIView(Border)
@end
**UIView+Border.m**:
```obj-c
#import "UIView+Border.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
@implementation UIView(Border)
- (id)swizzled_initWithFrame:(CGRect)frame
{
// This is the confusing part (article explains this line).
id result = [self swizzled_initWithFrame:frame];
// Safe guard: do we have an UIView (or something that has a layer)?
if ([result respondsToSelector:@selector(layer)]) {
// Get layer for this view.
CALayer *layer = [result layer];
// Set border on layer.
layer.borderWidth = 2;
layer.borderColor = [[UIColor redColor] CGColor];
}
// Return the modified view.
return result;
}
- (id)swizzled_initWithCoder:(NSCoder *)aDecoder
{
// This is the confusing part (article explains this line).
id result = [self swizzled_initWithCoder:aDecoder];
// Safe guard: do we have an UIView (or something that has a layer)?
if ([result respondsToSelector:@selector(layer)]) {
// Get layer for this view.
CALayer *layer = [result layer];
// Set border on layer.
layer.borderWidth = 2;
layer.borderColor = [[UIColor blueColor] CGColor];
}
// Return the modified view.
return result;
}
+ (void)load
{
// The "+ load" method is called once, very early in the application life-cycle.
// It's called even before the "main" function is called. Beware: there's no
// autorelease pool at this point, so avoid Objective-C calls.
Method original, swizzle;
// Get the "- (id)initWithFrame:" method.
original = class_getInstanceMethod(self, @selector(initWithFrame:));
// Get the "- (id)swizzled_initWithFrame:" method.
swizzle = class_getInstanceMethod(self, @selector(swizzled_initWithFrame:));
// Swap their implementations.
method_exchangeImplementations(original, swizzle);
// Get the "- (id)initWithCoder:" method.
original = class_getInstanceMethod(self, @selector(initWithCoder:));
// Get the "- (id)swizzled_initWithCoder:" method.
swizzle = class_getInstanceMethod(self, @selector(swizzled_initWithCoder:));
// Swap their implementations.
method_exchangeImplementations(original, swizzle);
}
@end