A while ago, Matt Neuburg started a thread on cocoa-dev by asking:

I'm just curious about how people are handling the fact that when you do KVO, all your notifications are bottlenecked through a single method, observeValueForKeyPath:... This is a very unpleasant and crude architecture (in contrast to NSNotification where, when a notification comes in, it is automatically routed to the selector of your choice). I really don't want a series of "ifs" here. I can imagine a simple dispatcher architecture based on NSSelectorFromString; is this the sort of thing people are using?

A deceptively simple solution to this problem is to put a selector in the context pointer like this:

- (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object 
    change:(NSDictionary *)change
    context:(void *)context
{
    [self performSelector:(SEL)context 
        withObject:[NSNotification notificationWithName:keyPath 
            object:object 
            userInfo:change]];
}

You write your method just like a NSNotificationCenter callback, and pass its selector when registering for the KVO notifications. Nice and simple.

However, as Chris Kane pointed out, this doesn't work when your superclass is also using KVO notifications. You are supposed to pass unknown notifications to super. Since we don't know how super has chosen to use the context pointer, it is difficult to use it for anything but identifying the notification:

static int uniqueAddress;

- (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object 
    change:(NSDictionary *)change
    context:(void *)context
{
    if (context==&uniqueAddress) {
        // handle notification for me
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

This resolves the problem of who should handle the notification, but now we can no longer use context for dispatching.

KVODispatcher

The real problem is that self is shared between you and your superclass (and subclasses, if any). To get around this, you can create a helper object and register that for notifications instead. The helper object will then forward notifications to a selector of your choice. Each class in an inheritance hierarchy can have its own helper object, so there is no problem with identifying who should get the notification.

The helper class would have an interface like this:

@interface KVODispather : NSObject {
    id owner;
}
- (id)initWithOwner:(id)owner;
- (void)startObserving:(id)object keyPath:(NSString*)keyPath 
    options:(NSKeyValueObservingOptions)options selector:(SEL)sel;
- (void)stopObserving:(id)object keyPath:(NSString*)keyPath;
@end

Since the helper derives from NSObject, it can safely put a selector in the context pointer:

- (void)startObserving:(id)object 
    keyPath:(NSString*)keyPath 
    options:(NSKeyValueObservingOptions)options 
    selector:(SEL)sel
{
    [object addOserver:self forKeyPath:keyPath options:options context:sel];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object 
    change:(NSDictionary *)change
    context:(void *)context
{
    [owner performSelector:(SEL)context 
        withObject:[NSNotification notificationWithName:keyPath 
            object:object 
            userInfo:change]];
}

This solves both problems: Notifications get dispatched to a selector of your choice, and there is no confusion about when to call super. It comes at the cost of allocating the extra helper object.

IMP Caching

If we consider the runtime speed of this solution, two issues pop up:

  • We are using [performSelector:withObject:], causing a doubling in the number of messages.
  • We are wrapping the arguments in an NSNotification because there is no three-argument version of [performSelector:withObject:].

IMP caching would handle both issues. What happens if we put an IMP pointer in the context pointer? It doesn't quite work; we are supposed to pass the selector as the second argument when invoking the IMP function, but we don't know the selector in [observeValueForKeyPath:]. How about a struct containing both the IMP pointer and the selector? That would work, but now we have to deallocate this struct in [stopObserving:keyPath:], and we can't get at the context pointer at that point.

Fortunately, the Objective C runtime comes to the rescue. If you take a look at the /usr/include/objc/objc-class.h header file, you will see that the struct objc_method contains exactly what we need -- an IMP pointer and a selector.

- (void)startObserving:(id)object keyPath:(NSString*)keyPath 
    options:(NSKeyValueObservingOptions)options 
    selector:(SEL)sel;
{
    Method m = class_getInstanceMethod([owner class], sel);
    if (!m) {
        [NSException raise:@"MethodMissing" 
            format:@"Class %@ has no selector %@", 
            [owner className], 
            NSStringFromSelector(sel)];
    }
    [object addObserver:self forKeyPath:keyPath options:options context:m];
}

- (void)observeValueForKeyPath:(NSString *)keyPath 
    ofObject:(id)object 
    change:(NSDictionary *)change 
    context:(void *)context
{
    Method m = (Method)context;
    (void(*)(id,SEL,id,id,id))(m->method_imp)(owner, m->method_name, keyPath, object, change);
}

You gotta love C syntax.

The beauty here is that the Objective C runtime owns the Method pointer, so we don't have to worry about freeing memory. As always with IMP caching, it won't work if you are changing method implementations by dynamically loading categories. Even worse, if you are removing methods (class_removeMethods), the Method pointer could become invalid. Don't do that.

Finally, the real reason for this post: I wanted to share the assembly generated for the observeValueForKeyPath method:

    .align 2
"-[KVOHelper observeValueForKeyPath:ofObject:change:context:]":
    lwz r12,8(r8)
    lwz r3,4(r3)
    lwz r4,0(r8)
    mtctr r12
    bctr

The overhead of dispatching through KVOHelper is three loads and a tail call. How neat is that?