Development, Mac OS, Obj-C, Programming

Objective-C Swizzling without Side Effects

<tl;dr>

Using method_exchangeImplementations results in a number of unexpected and dangerous side effects which in most cases you’ll want to avoid. You should use method_setImplementation and swizzle a C-style function instead to give yourself more control over your applications behaviour.

</tl;dr>

 

Swizzling is an incredibly powerful, and incredibly dangerous, feature of Objective-C that in the wrong hands can cause a serious amount of problems. A lot of this stems from a lack of understanding about what exactly swizzling is, and how it works. It’s also incredibly common to swizzle using a method that can cause quite serious side effects that if not tidied up can cause quirky and seriously hard to track down bugs.

I initially came across a few of these issues when unit testing our payment pipeline on iOS as I discussed previously. Swizzling worked like a charm, but required a bit of rooting around in the Apple docs to figure out why I was getting the behaviour I was.

Anyway, this post will cover a few of those problems I’ve just described

  1. What exactly is an Objective-C method
  2. Use method_exchangeImplementations at your peril
  3. Prefer method_setImplementation to swizzle without generating any side effects

 

What is an Objective-C method

Objective-C is built on C and as a result it’s methods are actually defined as C-style structs (typedef’d as Method)

struct objc_method
{
     SEL method_name;
     char* method_types;
     IMP method_imp;
}
typedef struct objc_method *Method;

The method_name is the name of the selector (as called by [self selector_name]), the method_types is an encoding of the return value and parameters passed to the function, but the most important entry for now is the method_imp.

This is how it’s defined in objc.h

typedef id (*IMP)(id, SEL, ...);

This means that every Obj-C method is actually just a C-style function that passes through the object, the command and the function parameters. So, if the method called uses any properties or other selectors (calling self. or [self selector_name]), it uses the id object passed through to the function which is where your problems can start to arise.

It’s this structure that’s key to understanding swizzling and the effects it can have.

 

The Consequence of method_exchangeImplementations

The most regular way of swizzling functions is through the use of method_exchangeImplementations. This does exactly as it says, swaps the two method implementations allowing calls to the original method to call the swizzled method instead.

@implementation my_interface

     -(int)swizzle_returns_22
     {
          return 22;
     }

     -(void)display_swizzle_results
     {
          // I’m using SKPayment* as it relates well to the next unit testing post
          SKPayment* payment = [[SKPayment alloc] init];
          NSLog(@“Default call: %d", (int)payment .quantity);
          
          // Get our methods to swap
          Method m1 = class_getInstanceMethod([payment class], @selector(quantity));
          Method m2 = class_getInstanceMethod([self class], @selector(swizzle_returns_22));

          // Exchange them
          method_exchangeImplementations(m1, m2);

          // Call quantity again
          NSLog(@“Replaced call: %d", (int)payment.quantity);
     }
@end

The output of display_swizzle_results is

Default call: 1
Replaced call: 22

But what if display_swizzle_results is modified to include an additional call to the replacement method?

-(void)display_swizzle_results
{
     // I’m using SKPayment* as it relates well to the next unit testing post
     SKPayment* payment = [[SKPayment alloc] init];
     NSLog(@“Default call: %d", (int)payment .quantity);
     
     // Get our methods to swap
     Method m1 = class_getInstanceMethod([payment class], @selector(quantity));
     Method m2 = class_getInstanceMethod([self class], @selector(swizzle_returns_22));
     
     // Exchange them
     method_exchangeImplementations(m1, m2);
     
     // Call quantity again
     NSLog(@“Replaced call: %d", (int)payment.quantity);
     
     // What is the result of this call?
     NSLog(@“Local method: %d", [self swizzle_returns_22]);
}

This new output is

Default call: 1
Replaced call: 22
Local method: 0

0 is probably not what you expected

  • If you didn’t see the 2 lines prior, you’d expect 22
  • If you did see the prior 2 lines, you’d expect 1

So we have two problem here, the first being the fact that you cannot possibly know who’ll use or modify your code in the future, and you’ve altered your code so any call to your functions actually calls something completely different.

The second is the call actually returns garbage.

The first problem is one of documentation and clarification. You could document the heck out of your definition and hope someone in the future bothers to read it, but it’s something a couple of breakpoints and a bit of wasted time would eventually figure out – “oh, it’s not actually calling that function”.

The second would be much harder to track down – “whats it calling and what the hell is 0?”.

So why is the call return 0?

We don’t have the source code to [SKPayment quantity] so we can’t look at the call directly, but that chances are that internally it’s using ‘self’ to access either another selector or another property. Since our implementations are called passing through ‘self’ those properties or selectors that the compiler thinks exists, actually don’t. It’s expecting an object or type SKPayment and instead it’s getting an object of type my_interface.

Who knows if what the internal call to self is calling something that exists in your object or doesn’t, or what the possible side effects will be.

None of this behaviour is ever desired, and should be avoided at all times.

 

Using method_setImplementation Instead

Using method_setImplementation avoids the side effects as it doesn’t swap the functions around (resulting in the call to your client function still calling your client function) and instead it simply replaces your target.

But if you look at the syntax of method_setImplementation it no longer takes a selector (it doesn’t need a new selector so why should it) so switching over isn’t as trivial as just changing the function name.

IMP method_setImplementation(Method method, IMP imp)

An IMP is nothing more than a function pointer that is explicitly passed the two ‘secret’ parameters.

typedef id (*IMP)(id, SEL, ...);

So, in our case, we simply need to define a C-style function to perform the behaviour of our original selector

int swizzle_returns_22_c_style(id self, SEL _cmd)
{
     return 22;
}

Now, to avoid any side effects or unexpected behaviour, here’s the original function again

-(void)display_swizzle_results
{
     // I’m using SKPayment* as it relates well to the next unit testing post
     SKPayment* payment = [[SKPayment alloc] init];
     NSLog(@“Default call: %d", (int)payment .quantity);
     
     // Set our new function
     Method originalMethod = class_getInstanceMethod([payment class], quantity);
     method_setImplementation(originalMethod, (IMP)swizzle_returns_22_c_style);
     
     // Call quantity again
     NSLog(@“Replaced call: %d", (int)payment.quantity);
     
     // What is the result of this call?
     NSLog(@“Local method: %d", [self swizzle_returns_22]);
}

This new output is

Default call: 1
Replaced call: 22
Local method: 22

The behaviour of these two functions is now significantly easier to understand, has zero side effects, and doesn’t result in us overriding behaviour we have no control or understanding of.

So, the next time you need to swizzle any default behaviour you otherwise wouldn’t have any control over, make sure you look at the available API and certainly default to preferring method_setImplementation over method_exchangeImplementations.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s