Dispatching KVO Notifications
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 toNSNotificationwhere, 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 onNSSelectorFromString; 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
NSNotificationbecause 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?