Objective-C caching and Method Swizzling
Posted by: Kevin Ballard in Cocoa, Programming, tags: c, caching, Cocoa, obj, objective, Programming(This post was adapted from an email I sent to Mike Solomon).
Objective-C has a method caching mechanism that optimizes for the case where
a small number of methods are called repeatedly on one (or more) objects of a
given class. This happens very often; for example, if you iterate over an array
to gather the results of performing an operation on each element, you’re going to
be calling the same method on a bunch of instances of the same class. And in fact
the -[NSEnumerator nextObject] method itself will also be cached.
The fact that method caching exists is common knowledge. However, what isn’t generally known is how that caching is implemented, and what it means for you if you want to hack around on the internals of a class.
Objective-C Method Caching
The biggest reason why people might care about Objective-C method caching is if they’re doing what’s commonly termed “Method Swizzling”. This is where you take two methods with the same signature (same return value and argument types), and you swap them so any code that tries to call the first will really call the second, and vice versa. The primary use of this is when you want to patch an app you have no control over - you can use a category to add a new method to the class, then swizzle that with the original method.
The reason why this is a potential issue is, since nobody knows how the method caching actually works, nobody knows if Method Swizzling can be affected by the method caching.
Well, the short answer is no.
Objective-C method caching works via a miniature hash table (of type struct objc_cache)
stored in the cache field in every struct objc_class. This hash table contains pointers
to recently-used Method objects, which are the same Method objects as found in the
struct objc_method_lists on each struct objc_class. The important part here is the
cached Method objects on a given class can actually belong to one of the class’s superclasses.
This is what makes the caching really useful — the method dispatch system doesn’t have to
trawl upwards through the class hierarchy to find the method if it’s already been cached.
The reason why Method Swizzling doesn’t have to worry about method caching is because of
the fact that the cached Method objects are the same ones from the struct objc_method_lists
in each class. When Method Swizzling modifies the Method object, the cache is modified at
the same time, so any subsequent cache hit will reflect this modified Method.
If for some reason you want to flush the cache for a class (and I can’t think of why), simply insert
void _objc_flush_caches(Class cls);
somewhere in your file and then call it, passing the class you wish to flush the cache of. This particular method will also flush the cache of every superclass of the given class.
Method Swizzling
If you want to use Method Swizzling in your own app (or Input Manager, or SIMBL plugin, or…) the following implementation is the best one I’m aware of.
void
{
// First, make sure the class isn't nil
if (aClass != nil) {
Method orig_method = nil, alt_method = nil;
// Next, look for the methods
if (forInstance) {
orig_method = class_getInstanceMethod(aClass, orig_sel);
alt_method = class_getInstanceMethod(aClass, alt_sel);
} else {
orig_method = class_getClassMethod(aClass, orig_sel);
alt_method = class_getClassMethod(aClass, alt_sel);
}
// If both are found, swizzle them
if ((orig_method != nil) && (alt_method != nil)) {
IMP temp;
temp = orig_method->method_imp;
orig_method->method_imp = alt_method->method_imp;
alt_method->method_imp = temp;
} else {
NSLog(@"PerformSwizzle Error: Original %@, Alternate %@",(orig_method == nil)?@" not found":@" found",(alt_method == nil)?@" not found":@" found");
}
} else {
NSLog(@"PerformSwizzle Error: Class not found");
}
}
void
{
PerformSwizzle(aClass, orig_sel, alt_sel, YES);
}
void
{
PerformSwizzle(aClass, orig_sel, alt_sel, NO);
}
This particular implementation swaps the method_imp pointers on the two methods, so calling the
first method will call the original implementation of the second, and vice versa. The elegant aspect
to this is when you want to call the original method — simply call your new method again. The following
example, adapted from SafariSource, demonstrates this.
- (void) mySetString:(NSString *)string {
NSUserDefaults *defs = ;
if () {
// do stuff here
} else {
;
}
}
When any code tries to call setString: on this class, the swizzled method mySetString: is called
again. But since the swizzling is done in both directions, trying to call mySetString: will in fact
call setString:. So when the code appears to be calling itself recursively, it’s really calling
the original function.
The biggest flaw with this implementation (and any others I’ve seen) is that if you want to swizzle a method that’s actually implemented in a superclass, the swizzling will affect all instances of that superclass instead of just instances of the subclass. There are a couple of possible solutions, such as dynamically creating a new class and using it to pose as the old class, but none have yet been implemented to my knowledge.

Entries (RSS)