The Right Way to Swizzle in Objective-C



The Right Way to Swizzle in Objective-C



RightWay_FINAL
Swizzling is the act of changing the functionality of a method by replacing the implementation of that method with another, usually at runtime. There are many different reasons one might want to use swizzling: introspection, overriding default behavior, or maybe even dynamic method loading. I’ve seen a lot of blog posts discussing swizzling in Objective-C, and a lot of them recommend some pretty bad practices. These bad practices aren’t really a big deal if you’re writing standalone applications, but if you’re writing frameworks for third-party developers, swizzling can mess up some basic assumptions that keep everything running smoothly. So what is the right way to swizzle in Objective-C?
Let’s start with the basics. When I say swizzling I mean the act of replacing the original method with my own method, and usually, calling the original method from within the replacement method. Objective-C permits this practice with the functions provided in the Objective-C Runtime. In the runtime, Objective-C methods are represented as a C struct called Method; a typedef of struct objc_method defined as:
struct objc_method
     SEL method_name         OBJC2_UNAVAILABLE;
     char *method_types      OBJC2_UNAVAILABLE;
     IMP method_imp          OBJC2_UNAVAILABLE;
}
method_name being the selector of the method, *method_types is a c-string of the type encodings of the parameters and return value, and method_imp is a function pointer to the actual function. (We’ll talk more about this IMP later.)
You can get access to this object using one of the following methods (more options available in the Objective-C Runtime):
Method class_getClassMethod(Class aClass, SEL aSelector);
Method class_getInstanceMethod(Class aClass, SEL aSelector);
With access to the Method struct of objects comes access to changing their underlying implementations. method_imp is of type IMP which is defined as id (*IMP)(id, SEL, …) or a function that takes an object pointer, selector, and an additional variable list of items as parameters, and returns an object pointer. This can be changed by using IMP method_setImplementation(Method method, IMP imp). Pass method_setImplementation() the replacement implementation, imp, along with the Method struct, method, you wish to modify and it will return the original IMP associated with that Method. This is the correct way to swizzle.

What is the incorrect way to swizzle?

Here is a power method commonly used to swizzle. While it looks straight forward—exchanging one method’s implementation with another—there are some non-obvious consequences.
void method_exchangeImplementations(Method m1, Method m2)
To understand these consequences, let’s look at the structure of m1 and m2 before and after this function is called.
Method m1 { //this is the original method. we want to switch this one with
             //our replacement method
      SEL method_name = @selector(originalMethodName)
      char *method_types = “v@:“ //returns void, params id(self),selector(_cmd)
      IMP method_imp = 0x000FFFF (MyBundle`[MyClass originalMethodName])
 }
Method m2 { //this is the swizzle method. We want this method executed when [MyClass
             //originalMethodName] is called
       SEL method_name = @selector(swizzle_originalMethodName)
       char *method_types = “v@:”
       IMP method_imp = 0x1234AABA (MyBundle`[MyClass swizzle_originalMethodName])
 }
These are the Method structs before we call the functions. The Objective-C code that generates these structures will look something like this:
@implementation MyClass
     - (void) originalMethodName //m1
     {
              //code
     }
     - (void) swizzle_originalMethodName //m2
     {
             //…code?
            [self swizzle_originalMethodName];//call original method
            //…code?
     }
 @end
We then call:
m1 = class_getInstanceMethod([MyClass class], @selector(originalMethodName));
m2 = class_getInstanceMethod([MyClass class], @selector(swizzle_originalMethodName));
method_exchangeImplementations(m1, m2)
Now the methods will look like this:
Method m1 { //this is the original Method struct. we want to switch this one with
             //our replacement method
     SEL method_name = @selector(originalMethodName)
     char *method_types = “v@:“ //returns void, params id(self),selector(_cmd)
     IMP method_imp = 0x1234AABA (MyBundle`[MyClass swizzle_originalMethodName])
 }
Method m2 { //this is the swizzle Method struct. We want this method executed when [MyClass
            //originalMethodName] is called
     SEL method_name = @selector(swizzle_originalMethodName)
     char *method_types = “v@:”
     IMP method_imp = 0x000FFFF (MyBundle`[MyClass originalMethodName])
 }
Notice how if we want to execute the original method code we’d have to call -[self swizzle_originalMethodName], but this results in the _cmd value being passed to the original method code to now be @selector(swizzle_originalMethodName), if the method code depends on _cmd to be the original name of the method (originalMethodName). This way of swizzling (example below) has impeded the normal functioning of the program, which should be avoided.
- (void) originalMethodName //m1
 {
          assert([NSStringFromSelector(_cmd) isEqualToString:@“originalMethodNamed”]); //this fails after swizzling //using
          //method_exchangedImplementations()
          //…
 }
Now let’s take a look at the proper way of swizzling – using the method_setImplementation() function.

The correct way to swizzle

Instead of creating an Objective-C function, -[(void) swizzle_originalMethodName], create a C function that conforms to the IMP definition (and more specifically to the signature of the method we are swizzling)† :
void __Swizzle_OriginalMethodName(id self, SEL _cmd)
 {
      //code
 }
we can cast this function as an IMP:
IMP swizzleImp = (IMP)__Swizzle_OriginalMethodName;
and this allows us to pass it to method_setImplementation():
method_setImplementation(method, swizzleImp);
and method_setImplementation() returns the original IMP:
IMP originalImp = method_setImplementation(method,swizzleImp);
Now, originalImp can be used to call†† the original method:
originalImp(self,_cmd);††
Here is an example of it all together:
@interface SwizzleExampleClass : NSObject
 - (void) swizzleExample;
 - (int) originalMethod;
 @end
static IMP __original_Method_Imp;
 int _replacement_Method(id self, SEL _cmd)
 {
      assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
      //code
     int returnValue = ((int(*)(id,SEL))__original_Method_Imp)(self, _cmd);
    return returnValue + 1;
 }
 @implementation SwizzleExampleClass
- (void) swizzleExample //call me to swizzle
 {
     Method m = class_getInstanceMethod([self class],
 @selector(originalMethod));
     __original_Method_Imp = method_setImplementation(m,
 (IMP)_replacement_Method);
 }
- (int) originalMethod
 {
        //code
        assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
        return 1;
 }
@end
It can be verified by doing this test:
SwizzleExampleClass* example = [[SwizzleExampleClass alloc] init];
int originalReturn = [example originalMethod];
[example swizzleExample];
int swizzledReturn = [example originalMethod];
assert(originalReturn == 1); //true
assert(swizzledReturn == 2); //true
In conclusion, to avoid conflicting with other third-party SDKs, don’t swizzle using Objective-C methods and method_swapImplementations(), but instead use C functions and method_setImplementation(), casting these C functions as IMPs. This avoids all the extra information baggage that comes along with an Objective-C method, such as a new selector name. If you want to swizzle, the best outcome is to leave no trace.
don’t forget, all Objective-C methods pass 2 hidden parameters: a reference to self(id self) and the method’s selector(SEL _cmd).
††you may have to case the IMP call if it returns a void. This is because ARC assumes all IMPs return an id and will try to retain void and primitive types.
IMP anImp; //represents objective-c function
          // -UIViewController viewDidLoad;
 ((void(*)(id,SEL))anImp)(self,_cmd); //call with a cast to prevent
                                     // ARC from retaining void.
*Sign image courtesy of Shutterstock

No comments:

Post a Comment