last edited August 22, 2009 08:08:56 (188.8.131.52)
|Edit / History / New / Search||Quick Links: Home / Recent Changes / Glossary / Jobs / Forums / Help|
ExtendingClasses - Lightweight modification of a single method at runtime
The Objective-C runtime lets you modify the mappings from a selector (method name) to an implementation (the method code itself). This allows you to "patch" methods in code you don't have the source to (AppKit, FoundationKit, etc). Unlike creating a category method with the same name as the original method (effectively replacing the original method), MethodSwizzling lets your replacement method make use of the original method, almost like subclassing.
This is best used in cases where a single method needs substitution or extension; If you need to modify many behaviors of a class, you may be better off using ClassPosing.
December 29, 2007: [http://mjtsai.com/blog/2007/12/29/jrswizzle/] Jonathan Rentzsch has started a project, JRSwizzle? [http://rentzsch.com/trac/wiki/JRSwizzle], to implement method swizzling correctly with different versions of the Objective-C runtime:
There’s at least four swizzling implementations floating around. Here’s a comparison chart to help you make sense of how they relate to each other and why JRSwizzle? exists.
For instance, let's pretend there's a class called Foo that implements the following method:
Now we've got applications that call [[Foo sharedFoo] fooBar] all over the place; We'd like to modify the functionality of the fooBar method to append something silly to the result of the original fooBar return value.
So, we'll implement a category method that does the work:
"But wait", I hear you saying, "the myfooBar method is calling itself, generating infinite recursion!" Well, that would be the case except for the fact that we're going to do some MethodSwizzling and swap the implementations of the two methods! Here's the deal: Right now, any code that calls -[Foo fooBar] is going to use the original implementation (let's call that "A"), and any code that calls -[Foo myfooBar] is going to use our new implementation (let's call that "B"). To summarize:
So right now, calling "fooBar" invokes implementation "A" like it always has; Calling "myfooBar", however, invokes implementation "B", which in turn calls "myfooBar", which invokes implementation "B", etc, leading to the unwanted infinite recursion.
After swizzling the two methods, we have the following situation:
Now, after the swizzling, anybody who calls "fooBar" will invoke implementation "B", which calls "myfooBar" (invoking implementation "A" and appending some silly text). In short, calling "fooBar" will now return @"username, bigtime luser"!
Hopefully this makes sense to people!
The MethodSwizzle function
Here's a C-function that does the trick:
Here's code that uses it:
Note the use of a category, just to check everything works correctly.
Here's the output as logged:
Hurrah! It works! I have also checked it by swizzling the init method of NSObject (successfully).
Indeed it does... but I wonder what difference it makes that you not only swap the IMP pointers in your implementation, but also the method_types... it seems to not make a difference. Anybody know of a case where this matters?
Hmmm, this works partly. I also tried to swizzle the init method of NSObject. Somehow the altInit I provided only gets called when an instance of NSObject is created. It definitely is NOT called for things like myString = [NSString stringWithString:@"test"];
This got me puzzled. I'm using GCC v3.3 and didn't have a chance yet to try with older versions. If anyone else has experience using these features I'd be interested in any information on this subject.
-- Jan Willem Luiten
Jan: A couple of points that might help:
First: The message [NSString stringWithString:@"test"] is equivalent to [[[NSString alloc] initWithString: @"test"] autorelease]. So to understand what's going on you need to understand how the +alloc and -init messages are implemented.
Second: NSString is a class cluster, which means that its +alloc method does not return an instance of NSString. It returns instead a (singleton) instance of a (private) subclass of NSString. (I think it's the NSPlaceholderString? class, but the exact name doesn't matter so let's just suppose that name is correct.) Thus the -initWithString: message is sent to the (singleton) instance of this (private) subclass and NOT to an instance of the NSString class.
Third: The message [NSPlaceholderString? initWithString: @"test] returns an instance of a concrete (private) subclasses of NSString, where the appropriate concrete subclass is determined by the form of the initialization message -- eg, -initWithString, initWithCString, initWithFormat, etc.
Except that normally every class's init methods start out by calling the superclass's init methods. However, you're right in this case. For whatever reason, setting a breakpoint shows that [NSString stringWithSting:@"test"] doesn't call through to -[NSObject init]. Maybe it's because of the bridging to CFString - a core foundation init method may be used instead.
Question: Why would Apple adopt use this sort of design?
Answer: They want to be able to optimize string objects by having one implementation of NSString's interface tuned specifically for C strings, another tuned specifically for Objective-C strings, etc in a way that encapsulates their implementation decisions. Proof of the effectiveness of this design is that you've been using the nice, clean, elegant interface of the public class NSString without any idea that you were working with instances of multiple private subclasses! Yet Another Proof of the great design of Cocoa frameworks ;-)
Works great, but for my project, I modified MethodSwizzle() to return an error code if one of the methods can't be found.
I suggest the following, to more closely match poseAsClass:'s interface...
Edit: Sorry, I made a bad mistake in the code last time that could actually cause a possible crash instead of an error message... that's what happens when you don't test code before you post it, I guess. What horrible form. ;-)
Is it really necessary to copy over the "method_types"? They should be same to the extent that the IMP your replacing should take the same arguments as the orginal, right? Otherwise, would there not be a problem with missing or extra arguments? Maybe, they would just be ignored or "nil/NULL" inserted as necessary?
Others such code examples, specifically that in Anguish et al. in "Cocoa Programming" at page 1083 and those archived at CocoaBuilder?, mention the need to call "_objc_flush_caches_(Class)" after swizzling to keep the runtime kosher. I have tried this and found that I still get some runtime strangeness, like specific confusion about inheritence and the states of ivars. After reading this page, which does not mention the use of this function, I ran my software without it and my ivar state problem appeared to go away. Does anybody know what the story with "_objc_flush_caches_(Class)" and method swizzling really is?
This seems to have one *huge* disadvantage: If a class inherits its method from its superclass, this code will replace the method in the superclass. So, if you swizzle a method in NSButton that it actually inherits from NSView, all other NSViews? will also have that method. So, you can't willy-nilly swizzle methods in several subclasses of the same class unless you are sure they all implement their own version. Moreover, if you do it with an Apple class and Apple moves the implementation into the superclass, you're screwed.
Personally, I find this whole thing rather hackish and have avoided it altogether. I am fully prepared to admit this may be ignorance or fear of the unknown talking, but it just seems wrong. ;-)
That's a very important point, Uli. We should work on this code and see what can be done. What comes to mind for me is that what we *really* want is dynamic subclassing + posing or isa-swizzling. FScript has this, though sort of in beta, and (I think?) PyObjC has it, so we just need to get an ObjC implementatation going.
Alternatively, one could do the following:
I guess the above should be corrected as follows:
If a class inherits its method from its superclass, add a stub method with the same selector that just calls the superclass' method. Now you can swizzle it like any other method and not have to worry about unintended side effects to other classes.
Hackish? Oh yeah.
-- Brooke C.
If you want to do this sort of thing with dynamically created classes take a look at my code at WeakPointers. The aforementioned code worked well so long as I didn't try it on any CoreFoundation classes.
I just wrote a new implementation of Method Swizzling that fixes the inherited methods problem. You can read about it here:
I updated Kevin's implementation to use the 10.5-style objc runtime API. - Aaron Harnly
and here it's used to implement an NSObject category adding swizzling methods to all classes:
The above code (Aaron's) had some problems, that, for me, resulted in a crash (maybe it works if you have GC or in some configuration; the problem was that at least one of the Method object used in the call to method_exchangeImplementations potentially points to a Method that is not the method created with class_addMethod). Hopefully, this will work - CharlesParnot
Paste this in replacement of original Kevin's code for the function _PerformSwizzle:
Note that the above code should probably use a plain old "unsigned int" instead of "NSUInteger" to match the signature of class_copyMethodList().
Make sure your calls to swizzleMethod only happen once. I made the mistake of putting the swizzle procedure in the '+initialize' method of a class in a category, and my swizzle procedure ended up running twice, effectively un-swizzling what had ben swizzled.
In researching this, it seems to me that the existing solutions for the Leopard runtime are extremely over-engineered. There are two cases to consider: the subclass implements the method you're replacing, or it inherits it. In the former case,
Seems foolproof, and of course is extremely simple compared to the other functions offered here. Have I missed something, or shall we remove those and leave this one as the example of the right way to do this? -- MikeAsh
Corrected omitted code in MikeAsh's code above. It does seem to work, at least with a quick test. Steve Weller.
|Edit / History / New / Search||Quick Links: Home / Recent Changes / Glossary / Jobs / Forums / Help|