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? } @endWe 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; }
@endIt 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); //trueIn 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