mikeash.com: just this guy, you know?

Posted at 2008-10-22 21:47 | RSS feed (Full text feed) | Blog Index
Next article: Don't Use NSOperationQueue
Previous article: The How and Why of Cocoa Initializers
Tags: cocoa kvo sourcecode
Key-Value Observing Done Right
by Mike Ash  

Cocoa's Key-Value Observing facilities are extremely powerful and useful. Unfortunately they have a really terrible API that's inherently broken in a couple of different ways. I want to discuss how it's broken, and a way to make it better.

What's Broken
There are three major problems with the KVO API, all of which relate to multiple levels of the class hierarchy registering observers. This is important because even NSObject (as part of its implementation of -bind:toObject:withKeyPath:options:) will observe things.

  1. -addObserver:forKeyPath:options:context: doesn't allow passing a custom selector to be invoked.
    If you look at similar APIs such as NSNotificationCenter, you'll see that registering an observer always involves passing a selector to be invoked when the specified event happens. This makes it very easy to separate things from a superclass, because you just direct the message to your own method. With KVO you have to override -observeValueForKeyPath:ofObject:change:context:, then either handle the message yourself or call super. Deciding whether to handle the message or pass it up the chain is complicated by the fact that super may have registered for the exact same key path and object.
  2. The context pointer is useless
    This is a consequence of #1. Because you can't specify a custom method to invoke, and you can't tell if your superclass will be interested in the notification by examining the key path or the object, you need some other way to tell if the notification is meant for you or for your superclass. The context pointer is how you do this. You must create a unique pointer that your superclass can't possibly be using, and then pass this to addObserver:.... You must then check to see if the context pointer is this unique pointer in your implementation of -observeValueForKeyPath:.... A consequence of this fact is that you can't use the context pointer to actually hold context.
  3. -removeObserver:forKeyPath: doesn't take enough parameters
    This method doesn't take a context pointer. This means that if you register for the same object/key path combination as your superclass, but with a different lifetime, you have no way of disabling only your observer. Calling this method may disable yours, it may disable the superclass's, or it could even disable both.
It's too bad that such a powerful tool is so broken. Especially since Apple is starting a trend of omitting traditional NSNotification and delegate callbacks in new APIs, instead simply supporting KVO. A perfect example of this is NSOperation: the only way to get notified when an NSOperation finishes is by using KVO to watch its isFinished property.

So what can we do about it? I don't want to complain without helping, so I've written a class to solve the problem. You can get it out of my public svn repository like so:

svn co http://www.mikeash.com/svn/MAKVONotificationCenter/

And you can browse it by just clicking the link above.

So how does it work? It takes advantage of one pointer that can be guaranteed to be unique: the 'self' pointer. Instead of registering the target object for a notification, it creates a unique helper object for each notification and registers that. The helper then receives the notification and bounces it back to the original observer. Since the helper is a unique object for each observation, it can hold metadata about the observation as simple instance variables and not need to rely on the context pointer, which can be the required unique pointer. Since the helper does nothing but listen for KVO notifications, the observation persists for the lifetime of the object and we can assume that its superclass, NSObject, either does not observe anything or will observe for the lifetime of the object as well.

MAKVONotificationCenter then bypasses all three deficiencies described above:

  1. A custom selector is provided in the -addObserver:... method which is invoked when the observed key path changes. The superclass will use a different selector, so the problem is solved. (And in the case of Cocoa superclasses, they'll be observing directly whereas MAKVONotificationCenter observes through a helper, ensuring that they won't interfere.)
  2. A userInfo parameter is provided which is passed into the observer method. This can be an arbitrary object containing any necessary information about the observation.
  3. The -removeObserver:... method takes not only the target and the key path, but also the selector. This way if both a subclass and a superclass register for the same key path on the same object, each one can de-register without affecting the other by specifying its unique selector.

There are some interesting features to note.

The +defaultCenter uses a simple lockless atomic call to make the singleton thread-safe without needing to lock every access. This is a nice technique which gives you a safe singleton without having to arrange to initialize it in advance or take the hit of locking and unlocking a lock every time the singleton is accessed.

A slightly nicer API is exposed as a category on NSObject. This is better than explicitly accessing the MAKVONotificationCenter singleton. In an extreme case, MAKVONotificationCenter could be removed from the header altogether, leaving only the NSObject extensions.

This code has essentially not been tested. The small bit of testing code in Tester.m is all I've done. Don't trust it until you've tried it. At about 150 lines of real code there isn't a lot to it, but caveat emptor in any case.

If you wish to use this in your own projects, you may do so as long as proper credit is given. And patches are most welcome if you discover deficiencies.

If you have any comments about the code please leave them below.

Did you enjoy this article? I'm selling a whole book full of them. It's available for iBooks and Kindle, plus a direct download in PDF and ePub format. It's also available in paper for the old-fashioned. Click here for more information.

Comments:

charles at 2008-10-23 13:35:58:
I totally agree with this post, and I am thankful for the code sharing. I did a similar thing 2-3 years ago when dealing with the Xgrid APIs (for some reason, one of the classes in these APIs does have a delegate, but none of the others, all KVO... go figure). See GridEZ.framework.

I believe some of the implementation choices made by Apple were based on performance (the KVO stuff gets called a lot by the UI, but maybe the fact that it gets called a lot is where they should have worked on), and was mostly designed to fit with all the bindings/InterfaceBuilder stuff. It works great for Interface Builder, but is horrible when you have to deal with it in code.

Jamie Kirkpatrick at 2008-11-02 19:47:07:
Great idea Mike. When I get back to Cocoa one of these days I'll be sure to use this.

Maybe you could point someone at Apple towards this piece in the hope that they might provide an improved API in the future? (or file a bug if that feels like it might work...)

Brian Webster at 2008-11-17 09:39:48:
Thanks a lot for the code, Mike. I'd been using a similar concept in my own code to dispatch KVO observations to individual selectors, but was never really satisified with it. This technique is much more robust.

I came across one problem in my application when using the code. The application uses Objective-C garbage collection, and while KVO would be triggered fine the first time a value changed, the notification would not come through on subsequent changes. Upon investigation, I found that the helper object was being collected, and the KVO reference to it had been zeroed out.

It looks like the cause of this was the OSAtomicCompareAndSwapPtrBarrier() call in initializing the shared notification center, which does not pass through a garbage collection write barrier. The whole notification center was being collected as a result, taking all the helper objects with it. My solution was just to add a CFRetain(center) call after a successful assignment, and that seems to make things run smoothly.

mikeash at 2008-11-17 10:03:10:
Nice detective work. I didn't really write with garbage collection in mind, but even if I had I would not have avoided this problem. The best workaround would be to use the objc_atomicCompareAndSwapGlobalBarrier() function defined in objc-atomic.h. As it is undocumented I can't be sure, but I believe this will also work in the non-GC case (although obviously such code would still require Tiger). Otherwise one function or the other could be chosen based on whether the collector is being used.

Of course your workaround works just fine as well. I think it's slightly uglier, but I have nothing against ugly but functional code.

Brian Webster at 2008-11-18 00:29:14:
Ah, I figured there was probably some objc_* invocation for such a situation, but I gave up pretty quickly after discovering little to no documentation for the new garbage collection APIs. I gave a try though, and it appears to work as expected.

mikeash at 2008-11-18 00:49:47:
Glad it worked. And just to correct my post above, such code would of course require Leopard, and wouldn't work on Tiger. Not a big concern if you're writing GC code anyway, of course.

Brent Gulanowski at 2009-05-06 04:30:58:
If the same object calls -addObserver:forKeypath:options:context: twice, doesn't the second registration replace the first? How can you bind to a keypath and observe it independently? The design of the API implies that doesn't work (and that your criticism of -removeObserver:forKeyPath: isn't valid)--the same object can only register to observe the same key path of an observed object once. If I'm just plain wrong, and someone has a link to the correct page in the docs, I'd appreciate it. Maybe I'll re-read them anyway.

As for problems 1 and 2, can't they be solved at the same time by using a selector (either SEL or string version) as the context? I only just thought of this. It's always bugged me having to write a switch or other logic in -observeValueForKeyPath: but I should have just included the selector as the context and then asked
if([self responsdsToSelector:(SEL)context]) [self performSelector:(SEL)context withObject:object withObject:change];


Still agree with you that the API could be improved a lot. But not with a notification center. Centralization is going the wrong direction.

mikeash at 2009-05-06 05:07:06:
I don't see anything in the documentation that says what happens if you observe twice. Absent any documented behavior, my assumption would be that it registers you twice. That's what happens with notifications, and is the sensible thing to do, at least if the context pointers are different. Otherwise it would be impossible for a class and a subclass to both observe the same thing, and that's just no good.

As for passing a selector as the context, that's pretty good. You'd have to be sure that no other class in your hierarchy would use it. In practice, using a unique selector name should be enough to guarantee that. (In other words, don't use @selector(description), use @selector(_myPrivateMethodNotApplesDoNotTouch).) I haven't seen this idea anywhere else before. I've seen a lot of talk about using a unique constant string so it should have been obvious, but wasn't.

As for centralization, you can think of it as an implementation detail if it makes you feel better. I guarantee you that Apple's implementation of KVO has some kind of central registry, they just don't show it to you.

Jerry Krinock at 2009-06-23 13:55:40:
Regarding the question of what happens if you register the same observation twice using Apple's -addObserver:forKeyPath:options:context:, I wrote some bonehead code and tested it. The answer is Mike is correct. Your -observeValueForKeyPath:ofObject:change:context: gets invoked twice. Also, you should invoke -removeObserver:forKeyPath: twice. Invoke it three times and you'll get a crash.

But the same experiment shows a problem when I try this with MAKVONotificationCenter. When -[MAKVONotificationCenter addObserver:object:keyPath:selector:userInfo:options] is invoked for the second time, it sets a second helper into its _observerHelpers dictionary using the same key as the first helper, thus replacing it. This causes the first helper to be deallocced, but it's still registered as an observer with Cocoa. So when a change is observed, Cocoa attempts to send it a message ... byebye!

Now, one might ask, who would be silly enough to register twice. Well, I have a Chicken and and Egg, both managed objects in a relationship. The user can create either the chicken or the egg first, and can change the chicken's egg, or the egg's chicken. Whenever a chicken has an egg, however it got there, I want the the chicken to be observing the temperature of its egg. In order to cover all the bases, I need to set this observation in several places: -[Chicken awakeFromInsert], -[Chicken awakeFromFetch], -[Egg awakeFromInsert], -[Egg awakeFromFetch]; also in the setters -[Chicken setEgg:] and -[Egg setChicken:] or else in the KVO methods observing these relationships. The result is that sometimes two identical observations get set. Maybe I could avoid the multiple identical observations somehow, but I haven't thought it all through yet.

I'd like to patch MAKVONotificationCenter so it can handle the multiple identical observations like Apple's does, and have a couple ideas but the hour is too late here to be doing conceptual work. I'll post now and see if anyone has any suggestions.

Jerry Krinock at 2009-06-24 00:55:13:
I decided to solve both my problem of adding redundant (multiple, identical) observers and MAKVONotificationCenter's problem of not being able to handle them, with one solution.

Well, I can think of no reason why an observer would want to receive the same observation twice in the same run loop iteration; it's a complete waste of CPU. Therefore, in my patched code, before setting a new observer, MAKVONotificationCenter checks to see if an observer with the requested parameters has already been set and, if so, doesn't set it. How will you know to not attempt to remove this observer which has not been set? Well, addObserver::::: now returns the instance of MAKVObservation (formerly the "helper") which it set. If it returns nil, you know that it didn't work and not to remove it.

But that's what I call the Hard Way. Rather than type all those parmeters again, you can stash the observation (into a mutable array instance variable, typically) and then when you're done observing, remove it using the new method, -[MAKVONotificationCenter removeObservation:].

But that's what I call the Middle Way because there's now there's an even easier way. A fourth irritating thing about Apple's KVO is that there is no way to remove all of an observer's observation at once, as you can with -[NSNotificationCenter removeObserver:]. This is usually what I want to do, in -dealloc or -didTurnIntoFault, when an object is going away, just remove all of its observations without having to worry about missing any of them. Thanks to the centralized storage of MAKVONotificationCenter, you can now do this using the new method -[MAKVONotificationCenter removeObserver:].

So, I tried this code in my actual project and it cured the crash I was getting yesterday. I still need to try removing observers the Easy Way.

Here's the project, with a new demo tool and documentation. Probably I'll find some bugs today -- it's an alpha alpha.
  
   sheepsystems.com/public_html/files/MAKVONotificationCenter.zip

Mike, if you could glance at my use of @synchronized one of these days that would be good. It looks like you were just trying to protect the helpers dictionary, but you know you're the multithreading guy.

Jerry Krinock at 2009-06-24 00:56:32:
Sorry, I pasted in my ftp destination instead of the link. Try this instead:

   http://sheepsystems.com/files/MAKVONotificationCenter.zip

mikeash at 2009-06-24 01:21:07:
Good stuff. Returning the observation object makes sense. I don't like exposing that object to the outside world, but since you leave it untyped, it's not much of an exposure.

Regarding your query about @synchronized, you are correct that its purpose is to protect the observations dictionary. Every access to that dictionary (read or write) must be wrapped in @synchronized(self). You missed one spot, on line 236 (in -removeObserver:object:keyPath:selector:). Everything else looks correct.

Thanks for sharing your changes. It's gratifying to see people doing something useful with this code.

Dave Camp at 2009-10-12 18:14:34:
So, does your SVN repository incorporate the changes Jerry made? I'm comparing the current code with his .zip and there are a lot of differences...

mikeash at 2009-10-13 03:13:35:
My copy does not incorporate Jerry's changed. I wanted to keep mine simple. I considered Jerry's version to be a fork (and a nifty one!) and didn't merge it back in.

halfactivist at 2009-11-13 13:34:50:
Actually, I wrote a simple workaround to allow passing a selector and this where the context comes in handy: in observeValueForKeyPath just call the selector you passed.
You could even create a simple class that provides a selector and other info and pass it in the context.

Gwynne Raskind at 2010-01-23 12:40:08:
I've modified Jerry's version of the code further. It drops support for 10.4 and can use a block for the KVO notification under 10.6. I also included a few OCUnit tests and automation for building the DocSet from the header docs. It's posted at:

http://www.darkrainfall.org/code.html

Steven Degutis at 2010-05-21 15:09:48:
Maybe I'm just too tired at the moment, but I don't follow your logic of the last few sentences of the "The context pointer is useless" bullet point. I don't see how you came to the conclusion that you can't store context in the context pointer. Could you clarify please?

Frédéric Testuz at 2010-08-12 11:53:38:
Inspire by this note and the new method for NSNotificationCenter in Snow Leopard, I did a simpler version with blocks. In my code, the observer objects are not collected. In consequence there is less problems with threading.

http://web.me.com/ftestuz/codes/FTZKeyValueObserver.zip

PS : I'm not used to share code, if I should have give credits in my code, please say it, I will correct the code.

taglud at 2010-10-27 06:54:39:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSString *action = (NSString *)context;

    if ([action isEqualToString:ANNOTATION_SELECTED_DESELECTED]) {
        BOOL annotationSelected = [[change valueForKey:@"new"] boolValue];

        if (annotationSelected) {
            // Actions when annotation selected.
            // I create the appropriate popover here and display it in self.view
        }
    } else {
        // Actions when annotation deselected.
        NSLog(@"Annotation deselected! But never pass here...");
    }
}

My problem is when my popover is dismissed, if I want to select the same annotation, it just doesn't work... Like if the state of the observer is still "activated". So in order to select my annotation, I need to select another one, and then I can select it again... It's annoying to not be able to select the same annotation twice in a row.

taglud at 2010-10-27 06:55:59:
my address is: taglud@gmail.com

Óscar Morales Vivó at 2011-10-09 20:59:51:
Readers might want to note that as of Lion/iOS5 there's a -removeObserver:forKeyPath:context: that takes care of issue #3.

With #3 taken care of and a little coder discipline the context pointer can find a use (dealing with #2) #1 can be partially taken care of. Everything still gets annoyingly funneled through ovserveValueFor… but by sending the class pointer as a context pointer to observe and checking for it at the observe method it is possible to avoid weird surprises.

YIem at 2016-05-18 02:39:46:
Thank you very much, let me further understand KVO


Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Name:
Web site:
Comment:
Formatting: <i> <b> <blockquote> <code>. URLs are automatically hyperlinked.
Hosted at DigitalOcean.