Wednesday, February 27, 2013

Method Swizzling

Objective-C is an amazing language.  So many possibilities with dynamic runtime typing and built in polymorphism.  But one of my favorite features of mutable runtime objects is the ability to Method Swizzle, which is effectively the ability to add a method or exchange 2 methods.  The usefulness of method swizzling is vast, such as adding a method to an older SDK that the newer SDK has or perhaps changing the functionality of an existing method.  In this post, I'll just give a quick implementation for method swizzling that can be reused over and over. A word of warning though, method swizzling can be very dangerous as you are effectively modifying the way existing code that has already been vetted works. 9 times out of ten, you can solve a problem without modifying the runtime and using object oriented patterns. Only when you have no choice should you consider method swizzling - and even then, you probably shouldn't.

Swizzling Methods for the win!







Method swizzling is something we can accomplish with the Objective-C runtime which has many powerful C functions in the runtime header.

#include <objc/runtime.h>

Now before we start coding it's important to have at least a high level understanding of how Objective-C methods work in the runtime.  When the objective-c runtime loads from a binary being executed, all the objects that are used are templatized - that is to say they have a their structure defined in memory with things such as what instance variables an instance has and the names of those instances (for KVC) and the instance methods and class methods a class has and the associated selector (SEL) used to call a particular method, which is effectively just a function that has 2 leading arguments: self, the pointer to the instance being executed on; and _cmd the selector that is executing.  This association of selectors and functions is maintained in a map for the object template.  This means that if we can change the function that a selector is associated with in the map to a new function we've got the amazing ability to change the behavior of objects at runtime.  Insane right!

Things that we will want to be able to do easily:

  1. If a method exists, we want to be able to exchange it with a new implementation that we can define in a category
  2. If a method doesn't exist, we want to be able to set our implementation as the method
  3. We want to do all of this for both instance methods AND for class methods
The biggest reason we want our functions to both be able to add the method if missing and replace the method if it exists is that when it comes to inheritance, it is very possible that you might want to swizzle out a method on a subclass that is implemented in a super class, in which case that class instance would NOT have the method for swizzling, but it's parent would.  It takes great care from the programmer to ensure that the method implementations are savvy enough to handle either scenario though.

Let's write out the function declarations for what we'd like to see and then tackle them one at a time.

BOOL SwizzleInstanceMethods(Class class, SEL dstSel, SEL srcSel);
BOOL SwizzleClassMethods(Class class, SEL dstSel, SEL srcSel);

So you can see the three necessary arguments for method swizzling, the Class to modify, the destination selector and the source selector.  Knowing the Objective-C runtime functions that can accomplish this yields a straightforward solution.  I'll address each, starting with instance method swizzling.


BOOL SwizzleInstanceMethods(Class class, SEL dstSel, SEL srcSel)
{
    Method dstMethod = class_getInstanceMethod(class, dstSel);
    Method srcMethod = class_getInstanceMethod(class, srcSel);
    if (!srcMethod)
    {
        @throw [NSException exceptionWithName:@"InvalidParameter"
                                       reason:[NSString stringWithFormat:@"Missing source method implementation for swizzling!  Class %@, Source: %@, Destination: %@", NSStringFromClass(class), NSStringFromSelector(srcSel), NSStringFromSelector(dstSel)]
                                     userInfo:nil];
    }
    IMP srcIMP = method_getImplementation(srcMethod);
    if (class_addMethod(class, dstSel, srcIMP, method_getTypeEncoding(srcMethod)))
    {
        class_replaceMethod(class, dstSel, method_getImplementation(dstMethod), method_getTypeEncoding(dstMethod));
    }
    else
    {
        method_exchangeImplementations(dstMethod, srcMethod);
    }
    return (method_getImplementation(srcMethod) == method_getImplementation(class_getInstanceMethod(class, dstSel)));
}

First, we will get the destination and source Methods.  We use the objc function class_getInstanceMethod to accomplish this.  Method is simply a pointer to a struct with all of the necessary method information disconnected from the selector used to call it.

Next, we validate that there is a source method to swizzle.  If the user hasn't implemented the source method as a category method, then there's no point in swizzling, so we'll throw an exception.

Then we get into the actual act of Swizzling.  The logic here is like this:

  • if the destination method doesn't exist, make the destination method be the source method
    • AND replace the old source method with the original destination method (which will be NULL and effectively removes the old source method)
  • if the destination method already existed, exchange the source and destination methods so they are both still in the runtime, but effectively have traded what selector they respond to

Finally, we want to validate success by ensuring the source method we'd set out to swizzle to respond to the destination selector does in fact respond to it with a simple pointer comparison.

Onto class method swizzling:


BOOL SwizzleClassMethods(Class class, SEL dstSel, SEL srcSel)
{
    Class metaClass = object_getClass(class);
    if (!metaClass || metaClass == class) // the metaClass being the same as class shows that class was already a MetaClass
    {
        @throw [NSException exceptionWithName:@"InvalidParameter"
                                       reason:[NSString stringWithFormat:@"%@ does not have a meta class to swizzle methods on!", NSStringFromClass(class)]
                                     userInfo:nil];
    }
    return SwizzleInstanceMethods(metaClass, dstSel, srcSel);
}

Wait, that seems too easy!  Well, the nice thing about class methods, is that they are merely instance methods on the Class instance of a Class, or the MetaClass.  So we just ensure that the class passed in has a metaClass that we can swizzle, and we're good to go!

Now one thing we could do is have a 3rd function that is effectively the same implementation as SwizzleInstanceMethods but with 1 additional parameter: BOOL isMetaClass.  With that function, call it SwizzleInstanceMethodsInternal, we could validate that the provided class was either the MetaClass (has a MetaClass equal to itself) or a normal class that has an actual MetaClass.  From there we could have our two functions call it with isMetaClass either YES or NO, but I'll leave that as an exercise for anyone wanted to go to that level.

There we go!  We can now easily swizzle one method for another in order to port functionality of a method forward or backward - or to overwrite the functionality of existing methods.  A word of warning, with this powerful tool in place, one must take great responsibility as modifying the object behavior can be dangerous if care is not taken.

I will follow this post up with a post to swizzle the description method on the NSData class so that it can behave in a configurable manner.
Find this code on github

4 comments:

  1. I have heard of method swizzling before, but never knew how it was done, thank you for this explanation. It will take some time to digest though.

    I do have a question about it though. As you said, "it is very dangerous" and "9 times out of 10 you can solve the problem without modifying the runtime". So my question is... when IS a time you should use method swizzling?

    I guess if you want to modify how many built in frameworks interact with the swizzled method, you may want it, but I'm not sure how adding something to have a built in framework use a modified NSString method would be conducive to not crashing. I'm sure it is useful, but why would I swizzle a method, instead of just making a completely new method in a category and use it instead for my classes?

    ReplyDelete
    Replies
    1. Method swizzling can come in useful when trying to modify the behavior of an existing method. For example, say you wanted to log the class name of every view controller (both belonging to your app and system provided VCs) whose view did appear. You could swizzle viewDidAppear:, have the swizzled method call the original implementation and add an NSLog(@"[%@ %@%@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (animated) ? @"YES" : @"NO") to get what you need done. This would be a case of modifying the runtime to improve your ability to debug/observe the behavior of your app.

      There are lots of reasons out there, but I can't simply enumerate them here - I hope this example can get you thinking about it. Otherwise, you should try a decision tree:

      1) Am I trying modifying the existing behavior of a system method? (Yes, continue)
      2) Can I add a category method to achieve what I want? (No, continue)
      3) Can I subclass in order to achieve what I want? (No, continue)
      4) Method swizzling is an option

      Last recommendation:
      Only swizzle to extend the behavior of a method. Don't ever change the underlying behavior of an existing method, since that is a dependency that the system will be expecting.

      Delete
  2. Thanks You for writing such a wonderful self explanatory blog. I have one question here:

    - When i am trying to access "self" in my swizzled method, it is not the object of my class rather it is object of the class whose method i swizzled. Is there a way to access actual "self" object..?

    ReplyDelete
  3. You should not swizzle across objects, that can have unintended side effects including not having access to the desired object instance with "self". Per the examples provided, you should swizzle a category method to replace an existing method for the same class.

    ReplyDelete