So a little earlier tonight Matt Drance opined about
claiming it wasn't so good. I personally love being able to specify a block to
handle a notification, and especially love being able to have the observer run on
the main thread, or on a queue of my choice. This is absolutely awesome.
However, he's right that there is a gotcha. As he explains here, there's a potential retain-loop:
As the comment in that code shows, there's a retain-loop happening there. The
self, which is retaining
observer. Until something calls
-[[NSNotificationCenter defaultCenter] removeObserver: self.observer] the object
is never going to have its refcount drop below 1. This wouldn't be a problem,
except we frequently use a pattern where an object signs up for a notification
which it expects to observe right up until it's deallocated. This works fine in
garbage-collected mode (where retain-loops aren't a problem), but under manual
memory management the object's
-dealloc method will never be called, thus the
observer won't be removed. Ouch.
The 'fix' for this is to put another method on the object which explicitly removes the observer first. But that involves more code than the old wordier way of handling notifications, so who wants to do that?
Remove observer in the block:
A second approach is to remove the observer from within the block. This is great (although only for events you're only interested in catching once), as you don't even need to have a member variable for the 'observer' returned from signing up– the block keeps track of it for you:
The downside here is that the notification must fire in order for the observer to be released. After signing up using this method, only the firing of this notification will cause all reference counts of the receiver to be removed. If the notification never fires, then the notification center will retain the block, and with it the observer object and the receiver. So that's not especially useful either.
On iOS, in fact, it's almost Zen: register for memory warnings, none occur, so observers all 'leak', eventually causing a memory warning purely from all the leaked observers, which removes them all.
Sure, it cleans up in the end– but so will a bunch of other stuff, which wouldn't have needed to but for all these observers clogging up the place. Sigh.
Remove observer in the block, but need to keep monitoring
The above is no use at all though if we want to keep monitoring a notification when it fires multiple times. To do that, we would need to remove the first observer and add another– but where does that leave us?
As you can see from the comments in the code, the issue becomes: what block can I now pass to the signup method? I want to pass the one which is currently executing, but how can I access it? Well, there's no simple way, although that's not to say there is NO way at all. However, it's a little bit convoluted:
To break it down:
- First of all we need a variable to point to the notification handler block, which
is accessible from within that block's definition– we define that up the top,
and we make it a
__blockvariable for good measure (and the sake of my continued sanity).
- We also need to be able to reassign the (initially on-stack)
tmpObserverobject from within this block, so we need to define that as a
- Then we define the lions share of the function inside a block which we just assign to this pre-defined variable (note the self-reference on the last line).
- Lastly, we sign up for the notification for the first time at the end of the function.
So that's that. But it still isn't actually flawless, because it still has the problem of the observer not being removed unless the notification is called. In fact, it compounds it, because it always registers another observer!
ARG ARG MAI BRANE HAZ TEH HURTINGZ
So what next? A timer to remove the observer and install a new one every now & then?
Will that have the same problem? Also, what if another instance of the notification
fires in between the
addObserverForName:... methods? Will
it get missed? Why, yes, it will.
However, I still love this API, because in certain situations these issues won't
really bite you. For example, an
NSOperation can remove its observers in its
-cancel method, and/or at the end of its
-start methods. If you've
got a singleton which is going to stay around effectively forever, or until a
particular method gets called (app terminating, going to background, etc.) then
you're obviously free to do exactly this sort of thing: either it'll never be
released, or it'll get a very particular message upon which to turn off.
But for normal classes, it's got the potential for pain and lots of it. For
instance, you might want to put it in a
UIViewController and remove the observer
-viewDidUnload. However, is that function guaranteed to be called? If a view
controller is popped, does it reflexively unload its view there & then, or
does it wait for either a memory warning or its own deallocation to do so? If the
latter, will it even call this? Well, it won't anyway, because this observer is
keeping the refcount too high for its
-dealloc method to be called in the first
In short: This function is wonderful for the case where you have a very simple thing to do when a notification fires, and when you want to specify that a notification should always fire on the main thread (OMG I love that bit– LOVE IT LOVE IT LOVE IT).
However, unless you have a clearly delineated means of removing the observation which doesn't rely on anything over which you have no control (i.e. the notification must fire, view must unload, etc.) then you need to be very careful about how you use it.
For your reference and edification, however, the complete source (written on a machine running Windows, so no it likely won't compile) is available here.
Just to really scare you: all the same things apply to any uses of
dispatch_source_t too. To avoid this, have the thing which 'posts' to the source
own it, and have something else fetch the source to install its own block. Which
mustn't reference the owner of the source (which is why you're using sources to
decouple your code, right? Right?).
Damn. I think I just depressed myself. *Sigh.*