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
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:
- In Tiger, mutation types for unordered collections (sets) was added.
- In Leopard, "initial" and "prior" observations, along with a much-improved means for registering dependent keys, were added.
- 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:
- With Snow Leopard, we got blocks, but KVO has completely ignored them.
- KVO's inability to deal with objects whose observations are never unregistered has never even been challenged.
NSNotificationCenter
got blocks and targeted observation removal (returning an object from the add call that could be used to remove that same observation later). KVO also missed that latter.- KVO never had a "remove all observers on this object" or "remove all observations registered by this object" semantic, making subclassing and extension that much more painful.
- KVO has no syntax for registering more than one key path at a time for observation. Or for observing more than one object at a time.
- Mike's solution (
MAKVONotificationCenter
) imposed an extra retain on both the observing and observed objects, leading to a tendency towards retain cycles and an inability to remove observations in adealloc
method.
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:
- It had to solve all three of Mike's listed issues, and therefore had to be based on his original implementation to at least some extent.
- It needed to support blocks as observer callbacks.
- It needed to support "automatic deregistration", i.e. removing any observations an object had registered, or had registered on it, during deallocation.
- It needed to be much easier to register multiple key paths, and key paths on multiple objects.
- It needed not to retain observer or observee.
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;
- A flag to turn off the automatic deregistration behavior. Since a tiny bit of runtime trickery was definitely going to be involved in that, I figured it'd be best to be able to shut it off on demand.
- A protocol for an "observation". This would be a generic object returned by observation registration methods that could be used to remove that specific registration. It could be queried as to whether the registration was still valid.
- A protocol for a "key path set". Any object could implement a method that returned a fast-enumerable object and thus be used as a "key path".
NSString
,NSSet
,NSArray
, andNSOrderedSet
got this support for free. - An object that represented the information for a single KVO notification, an
MAKVONotification
. This object would be passed to an observation block and provide convenient access to everything normally hidden in a change dictionary. The category on NSObject to provide the selector- and block-based registration methods, as well as the complementary unregistration methods. I also put in "opposite sentiment" methods, since I don't like KVO's order of doing things.
KVO's original methods go "
[target addObserver:observer ...]
". To me, telling the target of the observation to add an observer is backwards. So I added "[observer observeTarget:target ...]
" methods, which had the daunting and difficult task of sending the parameters toMAKVONoticationCenter
's methods in the opposite order.- In that same category, methods for unregistering observations based on any combination of observer, target, key path, selector, including none.
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:
__weak
isn't available on OS X 10.6 / iOS 4.x. While there are already other APIs in use (particularlyimp_implementationWithBlock()
) that break backwards compatibility, they're not that hard to work around if you want to, and there are other reasons not to use ZWRs.- You can't take ZWRs to a whole list of classes on OS X, including
NSWindow
. That would mean you couldn't use such objects as observers or make them the target of observations. __unsafe_unretained
is not unsafe in the presence of a swizzled-dealloc
method! Either observer or target will always remove the helper before it goes away, at which point it's gone from the other as well. And if the caller happened to pass the "I want to unregister manually" flag, the semantics of original KVO return: It was already illegal/a crashing bug to deallocate an object with registered observers. Making these proper__weak
references would only mask the problem temporarily, not solve it.
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!
Comments:
https://github.com/mikeash/MAKVONotificationCenter
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.
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).
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...
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).
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?
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 :(.
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.
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
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...
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.
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];
}];
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).
- 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?
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.
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.