mikeash.com: just this guy, you know?

Posted at 2012-03-02 14:09 | RSS feed (Full text feed) | Blog Index
Next article: Friday Q&A 2012-03-09: Let's Build NSMutableArray
Previous article: Friday Q&A 2012-02-17: Ring Buffers and Mirrored Memory: Part II
Tags: cocoa followup guest kvo semi-evil
Friday Q&A 2012-03-02: Key-Value Observing Done Right: Take 2
by Gwynne Raskind  

I'm back again for Friday Q&A, and this week I'm going to follow up on Mike's 2008 article, Key-Value Observing Done Right, where he debuted a replacement for KVO, MAKVONotificationCenter. It's been a long time since then, and it was high time such a useful piece of code got an update, which I gave it. With the help of Mike and Tony Xiao, it's gotten a full overhaul and is now a modern code library with some fun features. In this article, I'm going to go through the new stuff and how it was done.


Ruminations on Apple's efforts
Key-Value Observing has been around since Panther (10.3), and since then it has received three publicly-visible updates:

  1. In Tiger, mutation types for unordered collections (sets) was added.
  2. In Leopard, "initial" and "prior" observations, along with a much-improved means for registering dependent keys, were added.
  3. In Lion (and iOS 5), methods were added for removing registered observations based on a passed context.

In Mike Ash's article in 2008, circa Leopard, he described three major issues. Of those, only one has been solved since (the missing context parameter to observation removal). The other two, a lack of custom selectors and the uselessness of the context pointer, remain unsolved, and more issues have arisen since:

All of these are things Apple should have dealt with, and bugs filed against all of these limitations resulted in only one change in two major OS releases.

It is my personal opinion that KVO received so little attention because it was originally implemented as nothing more than a piece of the puzzle behind Cooca bindings. Cocoa bindings have been, in the opinion of many (including myself), a dismal failure in their intended purpose of making UI easy to wire up to code. It still works for the things Apple uses it for, and that's good enough for them.

That wasn't good enough for me, though.

Designing a better KVO
For a long time, I'd used MAKVONotificationCenter with some extra tweaks by the inspired Jerry Krinock (see the comments on Mike's original article and myself which added basic blocks support as well as both less and more specific observer unregistration. But that implementation was a bit inelegant and still suffered from the retain cycle issue. I finally decided to sit down and hack out something a little more formal.

The "new and improved" KVO needed the following features, in my eyes:

Implementing the new and improved KVO
The first step was to build the interface for this new version of KVO. Starting from MAKVONotificationCenter, I added;

Starting from Mike's implementation, adding blocks support was quite trivial:

    #if NS_BLOCKS_AVAILABLE
            if (_selector)
    #endif
                ((void (*)(id, SEL, NSString *, id, NSDictionary *, id))objc_msgSend)(_observer, _selector, keyPath, object, change, _userInfo);
    #if NS_BLOCKS_AVAILABLE
            else
            {
                MAKVONotification       *notification = nil;

                // Pass object instead of _target as the notification object so that
                //  array observations will work as expected.
                notification = [[MAKVONotification alloc] initWithObserver:_observer object:object keyPath:keyPath change:change];
                ((void (^)(MAKVONotification *))_userInfo)(notification);
            }
    #endif

The appropriate "register an observervation" routines simply passed the helper object a NULL selector and the block as the user info. MAKVONotification's implementation was very trivial, just passing it the appropriate data and adding a few accessors to grab the data from the dictionary.

The code for handling "key path sets" was also pretty trivial:

    NSMutableSet                *keyPaths = [NSMutableSet set];

    for (NSString *path in [keyPath ma_keyPathsAsSetOfStrings])
        [keyPaths addObject:path];

    _MAKVONotificationHelper    *helper = [[_MAKVONotificationHelper alloc] initWithObserver:observer object:target keyPaths:keyPaths selector:selector userInfo:userInfo options:options];

The key paths are copied into a set to avoid the requirement that the original key paths object (or the object it returns) have a persistent and immutable lifetime. The default implementations I provided for NSArray and NSSet, for example, return self as the "set of strings", and they could easily be mutable, which would be trouble later when the helper object needed to unregister its observation.

Making it possible to observe more than one object at a time was done by detecting an array target:

    for (NSString *keyPath in _keyPaths)
    {
        if ([target isKindOfClass:[NSArray class]])
        {
            [target addObserver:self toObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [target count])]
                     forKeyPath:keyPath options:options context:&MAKVONotificationHelperMagicContext];
        }
        else
            [target addObserver:self forKeyPath:keyPath options:options context:&MAKVONotificationHelperMagicContext];
    }

Rather than using a central dictionary of helpers to track observations, which required expensive string building and was unreliable in any case for blocks, I added each helper object to both the observer and target as an associated object:

    NSMutableSet                *observerHelpers = nil;

    if (_observer) {
        @synchronized (_observer)
        {
            if (!(observerHelpers = objc_getAssociatedObject(_observer, &MAKVONotificationCenter_HelpersKey)))
                objc_setAssociatedObject(_observer, &MAKVONotificationCenter_HelpersKey, observerHelpers = [NSMutableSet set], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        @synchronized (observerHelpers) { [observerHelpers addObject:self]; }
    }

Some may object to the condensed value-of-assignment syntax I used here, but it works. The @synchronized blocks make the operations thread-safe. Now removing a particular observation based on some combination of target, observer, key path, and selector becomes relatively simple:

    - (void)removeObserver:(id)observer object:(id)target keyPath:(id<MAKVOKeyPathSet>)keyPath selector:(SEL)selector
    {
        NSParameterAssert(observer || target);  // at least one of observer or target must be non-nil

        @autoreleasepool
        {
            NSMutableSet                *observerHelpers = objc_getAssociatedObject(observer, &MAKVONotificationCenter_HelpersKey) ?: [NSMutableSet set],
                                        *targetHelpers = objc_getAssociatedObject(target, &MAKVONotificationCenter_HelpersKey) ?: [NSMutableSet set],
                                        *allHelpers = [NSMutableSet set],
                                        *keyPaths = [NSMutableSet set];

            for (NSString *path in [keyPath ma_keyPathsAsSetOfStrings])
                [keyPaths addObject:path];
            @synchronized (observerHelpers) { [allHelpers unionSet:observerHelpers]; }
            @synchronized (targetHelpers) { [allHelpers unionSet:targetHelpers]; }

            for (_MAKVONotificationHelper *helper in allHelpers)
            {
                if ((!observer || helper->_observer == observer) &&
                    (!target || helper->_target == target) &&
                    (!keyPath || [helper->_keyPaths isEqualToSet:keyPaths]) &&
                    (!selector || helper->_selector == selector))
                {
                    [helper deregister];
                }
            }
        }
    }

First, get the list of helpers on both target and observer. I make heavy use of the fact that sending messages to nil is harmless and always returns nil to avoid extra checks, at the cost of a small allocation or two. Then build the list of key paths into a set, and combine the target and observer's helper lists as a single set, thread-safely. Because sets are unique collections, there will be no duplicate helpers in this combined list.

Now loop over all the helpers in that list and check whether each one matches the criteria specified - does it have the right observer, the right target, the right set of key paths, and the right selector? (Note: The code checks for exact equality of key path sets, rather than whether the helper being checked observes "any" of the paths passed instead of "all" of them. It didn't strike me that it'd be worth the extra effort to add the extra checks.) If so, deregister that helper, which will handle its own thread safety.

Automatically removing KVO notifications at object deallocation
The most complicated new feature of this improved KVO was no longer having to find a place to call [self removeAllObservations], even if that place was -dealloc. This was in spirit with ARC's lack of need to call [super dealloc]; I almost never need to remove an observation before the observer or target is deallocated, and keeping track of when to do so can get pretty arduous.

The most obvious way to always unregister observations at dealloc is to do it from the -dealloc method. That meant either dynamic subclassing or swizzling. Since KVO already does dynamic subclassing, that was a layer of complexity I wasn't prepared to delve into. I chose method swizzling.

My MAKVONotificationCenter calls -_swizzleObjectClassIfNeeded: on both the observer and target objects. It looks like this:

    - (void)_swizzleObjectClassIfNeeded:(id)object
    {
        if (!object)
            return;
        @synchronized (MAKVONotificationCenter_swizzledClasses)
        {
            Class           class = [object class];//object_getClass(object);

            if ([MAKVONotificationCenter_swizzledClasses containsObject:class])
                return;

            SEL             deallocSel = NSSelectorFromString(@"dealloc");/*@selector(dealloc)*/
            Method          dealloc = class_getInstanceMethod(class, deallocSel);
            IMP             origImpl = method_getImplementation(dealloc),
                            newImpl = imp_implementationWithBlock(/* ... snip ... */        
            class_replaceMethod(class, deallocSel, newImpl, method_getTypeEncoding(dealloc));

            [MAKVONotificationCenter_swizzledClasses addObject:class];
        }
    }

The first thing to do is check whether this particular class has been swizzled before. We want the class the object thinks it is here, not the class it actually is - with KVO, these differ, and swizzling KVO's dynamic subclasses turned out to cause very strange behaviors. If it has been swizzled, do nothing. I don't check whether a superclass or subclass of the given class has been swizzled before, because doing it multiple times in the hierarchy is harmless, resulting in a little extra useless work at worst.

Next, retrieve the SEL for -dealloc; it's not possible to use @selector(dealloc) in ARC mode, so I have to cheat the compiler with NSSelectorFromString(). I get the instance method for dealloc, and its original implementation. Then I create a new implementation from a block using Lion's delightful imp_implementationWithBlock() API - I'll describe the implementation itself below. Finally, I replace the the original dealloc on the class with the new one and add the class to the list of swizzled classes.

Here's the new dealloc implementation itself:

    (__bridge void *)^ (void *obj)
    {
        @autoreleasepool
        {
            for (_MAKVONotificationHelper *observation in [objc_getAssociatedObject((__bridge id)obj, &MAKVONotificationCenter_HelpersKey) copy])
            {
                // It's necessary to check the option here, as a particular
                //  observation may want manual deregistration while others
                //  on objects of the same class (or even the same object)
                //  don't.
                if (!(observation->_options & MAKeyValueObservingOptionUnregisterManually))
                    [observation deregister];
            }
        }
        ((void (*)(void *, SEL))origImpl)(obj, deallocSel);
    };

There's quite a bit to notice here. First, notice I cast the block itself to a (__bridge void *) - this is to keep ARC quiet about casting a block (which is an object) to its pointer form, as required by the C API.

Next, the block takes as its parameter a void pointer, rather than an id. This certainly seems counterintuitive; after all, the object passed to an implementation block is self! Once again, ARC has the answer. Under ARC, all parameters to a function (which a block is) are automatically sent a retain message when the function is entered. But since this is the implementation of a dealloc method, the result is attempted object resurrection, which corrupts memory. Much hilarity ensues. Forcing the object to be a plain pointer hides it from ARC. This was better than marking it as __unsafe_unretained because the semantics are clearer.

The entire body of the method is wrapped in an autorelease pool, as there's the potential for quite a lot of work to happen here and it'd be nice not to have all that sitting around until the next event loop.

Next, the set of helpers registered for this object is looped over. Remember, this set will include both helpers that have the object as an observer and those that have it as a target, since a helper is always added to both its observer and target. We check for the manual unregistration option, and if it's unset, unregister the helper. No thread safety is needed here; an object that is being deallocated can not be being used from multiple threads, or the program is already going to crash no matter what we do.

Finally, we call through to the original implementation of dealloc, which the block captured from its surrounding context. This is the beauty of using a block implementation; there's no need to stash the original dealloc anywhere at all; the block will do it for us. This is also why it's safe to swizzle subclasses. If somehow a class does get swizzled twice (say, a class that has a dealloc and then a subclass of it that doesn't), all that will happen is that two swizzled blocks will be called, followed by the correct original dealloc.

The helper is using __unsafe_unretained references! Why not __weak ones? What are you trying to pull here!?
If you've been looking at the code while reading this, you may have noticed that the _MAKVONotificationHelper object that does most of the magic holds __unsafe_unretained references to its observer and target. Wouldn't it make more sense to use the safer zeroing weak references ARC so helpfully provides?

Well, no. Here's why:

Who needs an observer at all?
During the development of all these changes, Mike Ash and Tony Xiao were following along fairly closely. Tony in particular came up with a particularly clever method in the NSObject category:

    - (id<MAKVOObservation>)addObservationKeyPath:(id<MAKVOKeyPathSet>)keyPath options:(NSKeyValueObservingOptions)options block:(void (^)(MAKVONotification *notification))block;

This method questions the basic assumption that there needs to be an "observer" object at all for KVO to work! With a block-based callback, the only important object is the one being observed. Who the observer is doesn't matter at all, and KVO's own internal requirements are satisfied with the helper object as the observer. While in practice the observer is still important, since the block will almost certainly reference it, there's no conceptually clear reason to have it there.

Tony was also responsible for the discussion that led to __weak observer and target references being replaced with __unsafe_unretained ones. He was a big help, and here's my shout out to him saying, "Thanks, Tony!"

Conclusion
The rest of the code in the file is pretty much boilerplate stuff and is fairly straightforward, so that wraps up my discussion, other than to mention that the unit tests I wrote were invaluable in making sure the code worked. Unit tests are a Good Thing, people!

That's all for this week for me, I'll be back in three weeks after Mike's two-parter on rebuilding Cocoa collection classes from scratch. As always, thanks for reading!

Did you enjoy this article? I'm selling whole books full of them! Volumes II and III are now out! They're available as ePub, PDF, print, and on iBooks and Kindle. Click here for more information.

Comments:

After all that, how about some code showing examples actual usage? Give me some ice cream for dessert after all that broccoli.
It isn't directly obvious from this article where to grab the code. For others looking for it, you can find it here:

https://github.com/mikeash/MAKVONotificationCenter
Oh man, swizzling dealloc seems bad. Especially as many classes now don't even implement dealloc (so you'd have to add it with a call to super, instead of swizzling). I have discovered an intriguing solution to this, which this margin is too narrow to contain.
This swizzling implementation should work fine on classes that don't implement dealloc. Did we miss something?
Swizzling dealloc to get save unregistrations is rather crazy to me. There IS already a native concept in Objective-C to manage an auxiliary resource, but I'll get to that.

My biggest quibble by far with the KVO interface is that it exposes a concept that requires resource management, but does not represent it as an object! This results in basically all of KVO's problems. If a KVO observation was an object, unregistering twice would be impossible. There would be no strange context argument. You wouldn't be able to affect the registrations of superclasses. You seem to have reached basically the same conclusion, but not as the core of the solution.

In http://overooped.com/post/7456709174/low-verbosity-kvo , I outline my own SPKVONotificationCenter. It returns KVOObservation objects: if that object is released, so is the KVO subscription, since the two are the same.

Thus, the way to make sure that a KVO observation go away when your subscribing object goes away, is to save your observation in a retained instance variable, and managed like any other ObjC object. No need to swizzle dealloc, no need for any magic, you just code ObjC like you always have.


If you are lazy and don't want to write lots of code such as an extra ivar and dealloc line (and you are, and you should be!), I do introduce one slightly magic concept in my macro $depends (or SPAddDependency, if you are allergic to macros). However, I don't swizzle dealloc, but instead I use ObjC's built-in concept for auxiliary resources: associated objects.

I also don't see the need of having a magic MAKVOKeyPathSet. Just build an abstraction on top of your KVO notification center. $depends does this too.

The end result is that you can type a single line of code that creates multiple registrations to multiple dependent objects, sets it up to be coupled to the lifetime of the calling object, and triggers a single Prior callback to a given block, and callbacks whenever any of the dependencies update:


$depends(@"draw center of gravity",
  physicsEngine, @"entities.position", @"entities.velocity"
  graphicsEngine, @"shouldDrawCoG", @"debuggingColor",
  ^{
    if(!graphicsEngine.shouldDrawCoG)
      return [graphicsEngine clear];
    [graphicsEngine setForegroundColor:graphicsEngine.debuggingColor];
    [graphicsEngine drawLineFrom:[physicsEngine.entities valueForKeyPath:@"@average.position"]
                    pointing:[physicsEngine.entities valueForKeyPath:@"@average.position"]];
});


Bindings are great a lot of the time. When they aren't, $depends is.
You may be able to avoid swizzling dealloc by attaching an associated object and then just doing your wicked deeds in *its* dealloc.
As crazy as it may seem, associated objects are not a solution here.

Using them was my first idea.

Unfortunately, as it turned out, associated objects are released too late in the object deallocation process. By that time, KVO has already marked any remaining observations as problems and spit out its warnings to console. It's not clear whether or not unregistering the observation in an associated object even works after that point, and it certainly wasn't considered acceptable to have to tell users "don't worry about the OS spewing warnings that you're risking crashes, it's lying", especially since there are circumstances in which those warnings would still be legitimate. Not to mention that apps which spam the console with spurious messages are generally considered to be poorly designed (I'm looking at you, Adium).
Hmm. That's a good point. The trick is not needed when you are subscribing to objects that you *know* will live longer than the object that is subscribing; but when that's not the case (e g if you are subscribing to a property of an object you own in an invar), it is.

In SPDepends, in these cases, we make sure to call SPRemoveAssociatedDependencies(self) as the first thing in dealloc. It's easy to forget, and something I've wanted to make less error prone. Swizzling dealloc is perhaps the best approach here :/

I'm not absolutely convinced though: asynchronicity is difficult, and you should always be very aware of where you start and where you should stop receiving async events. I might remove the associated objects feature from SPDepends and have its users always assign the dependency to an instance variable, so that its existence is explicit and clearly visible in code.

Sorry for the harshness of my first comment, I'm not sure why I wrote it like that...
nevyn: MAKVONotificationCenter has the option of explicit lifetime assignment (both by returning an "observation" object and by having the option not to do the swizzling). I agree that asynchronicity can be difficult; my insistence on doing the removal implicitly comes from experience, in that I've only quite rarely had cause to remove an observation anywhere other than dealloc - and most of the times when I have had to do with retain cycles.

(Note, I distinguish "having cause" from "having the possibility"; there are several times when I could have explicitly removed an observation earlier, but where it did no harm either performance or logic wise to leave it until dealloc, and it almost always simplified code to do so.)

I would have liked to avoid swizzling, of course, as I would expect any experienced Objective-C coder to do, but the fact (as with so many of Apple's frameworks) was that Apple had simply made it impossible, whether purposefully or not. KVO is too paranoid about leaving observers registered at dealloc time, perhaps correctly (though the phrase "observation info was leaked, and may even become mistakenly attached to another object" frightens me, making me wonder what sort of hackery is afoot to make that possible - I saw it happen several times while writing the unit tests).
Thanks for the awesome post, Gwynne! And what an interesting discussion in the comments!

I like your point that when using blocks an observer object suddenly has not that much use at all. However, there's a minor usability issue that it arises with it: an observer must be declared with the __block modifier or else it will be retained. I liked Jonathan Wight's approach to this issue: he uses a block, the first argument of which is called 'self'. When you want to add self as an observer, this argument shadows the actual self and so this actual self is not retained by the block, but can still be accessed via the shadowing 'self' argument.

I have used the trick in my own take on simplifying KVO with blocks. Here's the code -- https://github.com/alco/TastyKVO. I used dealloc swizzling to automatically remove observers too. And in the case of adding an observer, instead of returning an object that can be used later to unsubscribe from notifications, I store all the needed references in associated objects to make it possible to call 'removeAllObservers' on the target (the object being observer) or 'stopObservingAllTargets' on the observer, respectively.

All in all, I think we all agree on one thing: KVO is an extremely useful technology at its heart, but it was unlucky to get a universally despised API that forces developers to come up with their own wrappers. Is there a chance we'll get an official revamp from Apple some day?
Alex: Shadowing self is a neat trick, but unfortunately a non-starter for me; I always build with both -Werror and -Wshadow. I've found a lot of bugs that way.

I also store the observer helpers as associated objects, while returning the opaque reference; that way the caller can decide which method works better for them.

I'd love to see Apple take a hint from all these KVO wrappers, yeah :). Unfortunately, experience has shown that the likeliest way for that to happen is for all of us to file Radars and wait three OS versions :(.
@Gwynne: you're right, shadowing a local variable (albeit an invisible one) that triggers a warning may indeed prove inconvenient. But you still have a choice of naming that parameter something like 'self_' and using 'self_->ivar' inside the block.

One could argue that the '__block' approach is prettier, but I'd say that passing the observer as an argument to the block grants you some level of safety as it eliminates the possibility to accidentally change the value of 'self' inside the block.
While a big user of some MA* classes, I've never quite been happy to use MAKVONotificationCenter for a few reasons, including the fact that a central notification center doesn't seem like the right model. Unlike NSNotifications KVO is never distributed nor multicast - it's purely point to point.

The comment from Joachim got me thinking about modelling the observation as an object, but Gwynne pointed out some issues with SPKVONotificationCenter (and there's that notification center concept again).

Enter PMPKVObservation! Totally standalone .m/.h pair that you can include in your project. As the observer you are responsible for managing the lifecycle of the observation, except where it comes to the possibly early release of the observed object (in which case a swizzle+associated object takes care of it for you).

It's about as minimal as I think it can be and still be effective. I haven't used it in too much anger yet so I'd love feedback.

https://github.com/aufflick/PMPKVObservation
Thanks for the thoughts on swizzling dealloc, Gwynne. I've been thinking about it a lot, in particular in relation to autorelease problems we are having in our code base (depending too much on the associated object, and the observation being invalidated just slightly too late), and I've concluded that you're completely right. Swizzling still makes me scared, but I'll try it out next time I'm doing infrastructure work.

Mark Aufflic: Indeed, the "Notification Center" is completely superflous, I have no idea why I wrote it like that. As you can see in my code, I don't use any ivar storage at all, and on top of that have an NSObject category that removes the Notification Center concept.

Your implementation looks very simple and straightforward :) Thanks for the code, might reuse some of it in mine, if that's alright...
Like most other comment authors, I also have my own rolled version of KVO to handle auto-removal and blocks - you'd think with the focus on ARC and blocks that it would have been addressed. I have gone to great lengths to provide optional "on-dealloc" usage options which provide using associated objects and/or swizzling dealloc. I have provided options, mainly because of my question about iOS (see a few paragraphs later).

I will say that I did not know about imp_implementationWithBlock before reading this post. Man, I really like that.

Using associated objects is the most "orthodox" method in that it has been specifically blessed by Apple. However, the associated objects are removed after all inherited dealloc calls. The memory has yet to be released, but just about everything else in the dealloc chain has happened.

This is OK in some aspects, but for unregistering KVO it comes too late in several regards (for both observed and observer).

However, my main issue with swizzling dealloc is for iOS, and the reported denial of applications that swizzle dealloc. Since I've yet to see an authoritative stance in writing (or video), I'm a bit undecided as to how I should approach swizzling dealloc.

Can anyone help shed some light as to Apple's stance on apps that swizzle dealloc? All I can find are posts about some apps being denied, but nothing authoritative.
Thanks for this great piece of code to Gwynne, Mike and Tony.

I have a question: is my understanding correct that, while MAKVONotificationCenter avoids retain cycles that are related to unregistering KVO, it does not (and cannot) deal with retain cycles that occur because I used the observed object or the observer inside the notification block?

So code like the following will lead to a retain cycle unless I use __weak id weakSelf = self;?

[self addObservationKeyPath:@"someProperty" options:0 block:^(MAKVONotification *notification) {
    [self doSomething];
}];
Ole: Correct; you have to manage the retain cycles in your blocks yourself, as with any other API's block-based callbacks.

However, one less obvious way around the problem is to make use of the notification object's observer and target properties, which will not cause a cycle because the block doesn't need to capture them from the enclosing scope. The down side to that, of course, is that you lose the static typing of your objects unless you do something like __typeof__(self) observer = notification.observer; at the top of the block (which doesn't cause a retain cycle because typeof is purely a compile-time expression).
Great work. Q: What's the best way to remove an observation the first time it triggers? I've found need to do that a few times, the most interesting of which is for this idiom (please advise if there's anything fundamentally stupid about it):
    - doAThingWith:(id)x
      if (decision)
        [x doSomething]
      else
        [otherThing addObservationKeyPath:@"property" ..block:^(..) {
          // i want to delete this very observation here
          // and within this next recurive call, perhaps re-observe
          [self doAThingWith:x]
        }]

Target and keypath are not likely to be unique, and if addObserver was used instead of addObservationKeyPath then even the obvious observer might not be unique either. In other words, I'd really like to use be able to do [observation remove] instead of [target removeObservation:..] or [observer stopObserving:..]

Are retain cycles the only thing to watch out for when doing something like:
    observation = [otherThing addObservationKeyPath ... {
        [observation remove];
    }];

I've also played with adding an observation property to MAKVONotification and also adding an ObserveOnce option, does anyone else see benefits or dangers to doing either?
jpmhouston: Here is the pattern I typically use for a once-off KVO:


id<MAKVOObservation> __block observation = [thing addObservationKeyPath:@"property" ... block: ^ (MAKVONotification *notification) {
    [observation remove];
}];


Marking the observation __block insulates the value from being captured too early, and should leave ARC doing the right thing as well.

A retain cycle does result in the current version of the code. The cycle can be solved by adding "userInfo = nil;" to the end of the -deregister method in MAKVONotificationCenter.m, which will nil out the block when the observation is unregistered, killing its retain and allowing the observation object to be deallocated as well.

(I would suggest submitting a pull request with this change, as this is probably a common pattern.)

This still results in a memory leak in the case where the observation never fires, but the leak is nullified when the target is deallocated and the observation is automatically deregistered.
This is great stuff. I really appreciate the articles and the comments. One problem that doesn't appear to be addressed, however, is the maintenance of KVO keys that are observed. Here are a couple scenarios:

1. Specifying the wrong value for the key and therefore you never get called, or you don't recognize that you got called.

2. Renaming keys and missing a reference.

These sorts of problems typically are only found at some later point when you're working on something else and one of your users 1-stars your app because you were a bonehead and made one of these errors. Ideally, these would be discovered at compile time.

You might say that KVO was not intended for this, that it was intended only as a loosely-coupled notification mechanism. True, but can we get the best of both worlds?

What do you think?

Thanks,
David

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:
The Answer to the Ultimate Question of Life, the Universe, and Everything?
Comment:
Formatting: <i> <b> <blockquote> <code>.
NOTE: Due to an increase in spam, URLs are forbidden! Please provide search terms or fragment your URLs so they don't look like URLs.
Code syntax highlighting thanks to Pygments.
Hosted at DigitalOcean.