Proper Key-Value Observer Usage
One of the things I like about writing for MacTech is that it causes me to evaluate my own knowledge. When writing “The Road to Code”, I’m kinda obsessed with making sure I’m doing things the “proper” way. I figure, since I’m giving guidelines and advice to new Objective-C and OS X programmers, I don’t want to start them off on the wrong foot.
For one of my latest articles, I was writing about Key-Value Observing, or KVO. KVO allows one object to register for notifications when a key of another object changes. I was writing code to register as an observer, and I realized that I wanted to make sure I was doing it right. As it turns out, my understanding, the documentation, and some sample code had holes.
I was writing code to add an observer using the addObserver:forKeyPath:
method. Here’s the full method signature:
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
It seems like such a straight forward method, yet there are some tricks to using it properly. The biggest gotcha is that all notifications get funneled through a single method in the observer:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
This is not like NSNotification
-based notifications, where you can register different selectors for each notification. Of course, if you register multiple KVO notifications, you need some way to differentiate them. One straightforward way, is to check the keyPath
. This is even what Apple’s documentation currently says (as of 24-Sep-2008):
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:@"openingBalance"])
{
// Do something
}
[super observeValueForKeyPath:keyPath ...];
}
This code has a couple potential issues. First, you can register the same keyPath
on multiple objects. Second, unconditionally calling [super observeValueForKeyPath:...]
is dangerous. For example, the NSObject
implementation throws an exception, so following Apple’s documentation will result in runtime errors.
To fix the second problem, only call super
if you do not handle the notification:
if ([keyPath isEqual:@"openingBalance"])
{
// Do something
}
else
[super observeValueForKeyPath:keyPath ...];
To deal with the first problem seems easy. You could check the object
that’s passed in as well. Unfortunately, this doesn’t help the case where a subclass or superclass has registered a notification on the same key path of the same object.
If, however, you specify something unique for the context, you can use that to correctly identify your notifications. So, what should you use as the unique context? For a long time, I’ve used a simple string:
static NSString * kOpeningBalanceChanged =
@"openingBalance changed";
You can then check for this string when you receive a notification:
if (context == kOpeningBalanceChanged])
{
// Do something
}
The problem here is that another class in your class hierarchy my be using the same string literal. The linker will consolidate all string literals with the same contents across all modules, thus the NSString
instance is shared amongst the static instances. Bad. Now it’s not unique to your class.
The solution to this problem is to provide unique string literal contents. One good way to do this is to put your class name in the string:
static NSString * kOpeningBalanceChanged =
@"MyClass openingBalance changed";
This works, but the you need to remember to change the string if your class name changes. This could be error prone, as you may forget to change the string. Even refactoring tools will most likely miss this.
As it turns out there’s an easier way to provide a guaranteed unique context value: use the address of a static variable. Each static variable is given a unique spot in your program and thus has a unique address.
if (context == &kOpeningBalanceChanged])
{
// Do something
}
I learned of this technique on cocoa-dev. It’s not something I thought of initially, so I’m glad I asked. In fact, you don’t even need to supply a string literal, since you only care about the address. Both of these would be correct alternatives:
static NSString * kOpeningBalanceChanged;
static void * kOpeningBalanceChanged;
The benefit here is that you don’t waste space for storing the string. The downside, in my mind, is lack of debugability. If you set a breakpoint in observeValueForKeyPath:
and you print out the context
, you just get a hex address back. There’s no easy way in gdb to correlate this address with a particular context variable.
To get back the ability to debug, you can combine the two approaches. Use an NSString
literal plus the address of the static variable. The contents of the string need not be unique, so you can just do something simple, like:
static NSString * kOpeningBalanceChanged =
@"openingBalance changed";
Now, you can use “po *(id *)context” to print out the contents of the context in gdb. To me, this is the best of both worlds, and the storage wasted for the string contents are worth it.
If you really wanted to get fancy, you could even create a macro using the stringizing operator:
#define DDDefineContext(_X_) static NSString * _X_ = @#_X_
You would use it like:
DDDefineContext(kOpeningBalanceChanged);
And if you wanted to get totally crazy, you could provide an alternate macro definition for Release builds that does not include the string literal to save space. Isn’t the C preprocessor fun?
BTW, I have filed rdar://problem/6185473 so that the KVO documentation gets updated to explain how to properly use the context. Also, for some good discussion on this topic, read these threads on cocoa-dev: [1], [2].