Method Swizzling is the ability that Objective-C Runtime gives us, to switch the implementation of an existing selector at runtime.
For example, for some weird reason we need to monitor “write operations” for NSUserDefaults. And by “write operations” I mean:
[[NSUserDefaults standardUserDefaults] setObject:@”A value” forKey:@”aKey”];
That’s easy! We are going to use a Subclass or a Category!
For this example we are going to use a category and extend the NSUserDefaults. We are going to create a method mySetObject:forKey:.
So this is our category:
and this is how we use it:
So this is straight forward. We extended NSUserDefaults with a new method that calls the original setObject:forKey and also logs the “write operarations” using NSLog. So in order to log all “writes operation” we are going to use our new method instead of setObject:forKey.
We solved the problem, now what?
We actually solved a part of the problem. If we need to monitor all “writes operations” from the code we wrote, this category will do the trick.
But let’s assume that we imported Firebase framework which is saving some properties using NSUserDefault(I don’t know if Firebase is really using NSUserDefaults but lets assume that for the example).
It’s clear that we have a problem. We can’t log Firebase calls to setObject:forKey.
So we have the following options:
- Send email to Google and ask them to use our mySetObject:forKey.
- Use method swizzling.
- Some other hacky solution that I can’t think of right now.
So obviously we will proceed with option two(But if you decide to send email to Google please cc me. I would like to see their reaction 😂).
What we did here is to method swizzle through a category. That means that we replaced NSUserDefaults’ setObject:forKey: implementation with our own swizzled_setObject:forKey: which is basically calling the original setObject:forKey: and then prints the object that was added in NSUserDefaults.
+(void)load class method
Load is class method which is invoked by the Objective-C Runtime when then class is initially loaded. Because method swizzling affects the global state, we perform it on load class method as a precaution against Race Conditions.
If you are familiar with Singleton you already know dispatch_once. dispatch_once quarantees that this piece of code will be executed once across different threads. Again, one more precaution against Race Conditions.
Method, Selector and Implementation
Selector is a C string that represents the name of the method at the runtime. At lines 1-2 we are getting the selectors for the two methods that we need to swizzle.
Method is an entry to the dispatch table of the class, which maps the Selectors (as a key), with their Implementations (as value). At lines 4-5 we are getting a reference to the methods that we want to swizzle later.
Implementation is a pointer to the start of a C function that implements the method. At line 7 we use method_getImplementation() to get the pointer that points on the start of the function of our swizzled method.
If we can visual the relation between the 3 of them and the Class’ Dispatch table it would look like this:
The tricky part
So here is the tricky part where the swizzle happens. Lets analyze each line to clear up the confusion.
On line 1 we call the class_addMethod() which is adding a method on a class at runtime and returns a boolean if the “add procedure” was successful. So why we are trying to add a method that already exists(if you put a breakpoint on defaultMethod you will notice that is not empty)? If you look closely to the parameters you will notice that we are trying to add a method with defaultSelector (which is mapped with the setObjec:forKey:) and map it with swizzle’s implementation! class_AddMethod will return “false” if the defaultSelector already exists in the class’ dispatch table. On our case will return “false” but we reverse the output with “!” so isMethodExist will be “true”. On line 4 we use method_exchangeImplementation to swap the implementation of our defaultMethod with the swizzledMethod.
But lets go back on line 1 and assume that NSUserDefaults it’s a subclass of ParentNSUserdefaults(made up for this example) which has setObject:forKey: method and NSUserDefaults is not overriding it. class_getInstanceMethod([self class], defaultSelector) for the defaultMethod would return the method of the parent class. And if we use method_exchangeImplementation, we will mess up parent’s method(and probably crash will occur). On this class_addMethod, we add the method with the selector defaultSelector and implementation of swizzledMethod. class_addMethod will return true and the class_replaceMethod will be executed, which is replacing swizzledMethod’s implementation with parent’s implementation without messing up anything. So, we added the defaultSelector method on NSUserDefaults with the implementation of the swizzleMethod and then we replaced swizzleMethod’s implementation with ParentNSUserDefaults’ setObject:forKey: implementation.
If you still don’t get it, download the project at the end of story which includes an example of UIViewController.
The swizzled method
Your programmer instict should already kicked in, warning you that this method will run infinitely, but it won’t. Because at runtime swizzled_setObject:forKey: selector would point on setObject:forKey:. Then we have an NSLog to log our command as we did with the Category example.
Now for any setObject:forKey call on NSUserDefaults from our code or from a framework, our swizzled method will executed.
So, to swizzle or not to swizzle?
It’s your call! But I will help you take that call by sharing some considerations:
- PLEASE DON’T copy/paste the solution in your project and ship it, if you don’t know what’s going on.
- Troubleshooting will be very difficult. You might get it working for a while and then after importing a framework that swizzle the same method, your app is crashing(Unlikely but it’s a possibility). For example, Firebase for iOS is performing some method swizzling but they give you the choice to disable it.
- It could break on the next release without any notice.
- I would avoid using it on a Framework that is used by other developers. Lets say that you are working on that framework that is used by 1000 apps. You miss a little detail and BOOM, 1000 apps are crashing because of your framework. Other than that, you can’t know if the developer who is using your framework is trying to swizzle the same methods.
- Note that when you swizzle a method the _cmd value will show the swizzled selector. So if you put breakpoint in swizzled_setObject:forKey you will notice that the _cmd value is setObject:forKey.
- The order of swizzle matters, so be careful when swizzling multiples classes. For example if you swizzle UIView and UIButton you must be careful because UIButton inherits from UIControl which inherits from UIView. I would also recommend to take a look at Apple’s Documentation about the order that load messages are sent.
You can find the project on Github (Including the UIViewController example and the subclass that miss the method to swizzle).
If you enjoyed this article check out our publication Rock n Null about mobile and technology!