An NSNotification + Blocks Gotcha

So a little earlier tonight Matt Drance opined about -[NSNotificationCenter addObserver...usingBlock:], 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:

- (void) storeObserverVariable
{
    self.observer = [[NSNotificationCenter defaultCenter]
                     addObserverForName: @"TheNotification"
                                 object: nil
                                  queue: [NSOperationQueue mainQueue]
                             usingBlock: ^(NSNotification * note) {
        // this reference to 'self' within the block causes 'observer'
        // to retain 'self' which retains 'observer', which retains the
        // block, which retains 'self' ...
        if ( [self validateContents] == NO )
            return;
        [self sortContents];
        [self performLayout];
    }];
}

As the comment in that code shows, there’s a retain-loop happening there. The block retains 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:

- (void) stopObservingInsideBlock
{
    id tmpObserver = [[NSNotificationCenter defaultCenter]
                      addObserverForName: @"TheNotification"
                                  object: nil
                                   queue: [NSOperationQueue mainQueue]
                              usingBlock: ^(NSNotification * note) {
        if ( [self validateContents] == NO )
        {
            // must remember to remove observer if there are multiple
            // exit paths (which are often a bad thing in themselves,
            // but...)
            [[NSNotificationCenter defaultCenter] removeObserver: tmpObserver];
            return;
        }

        [self sortContents];
        [self performLayout];

        // we must remove observer from within this block, because
        // nothing else has a reference to it otherwise.
        [[NSNotificationCenter defaultCenter] removeObserver: tmpObserver];
    }];
}

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?

- (void) repeatObservationsWithoutMemberVariable
{
    id tmpObserver = [[NSNotificationCenter defaultCenter]
                      addObserverForName: @"TheNotification"
                                  object: nil
                                   queue: [NSOperationQueue mainQueue]
                              usingBlock: ^(NSNotification * note) {
        if ( [self validateContents] == NO )
        {
            // must remember to remove observer if there are multiple
            // exit paths (which are often a bad thing in themselves,
            // but...)
            [[NSNotificationCenter defaultCenter] removeObserver: tmpObserver];

            // I want to sign up again, getting another observer object
            // to release on the next invocation -- but how can I
            // reference the block that's currently executing to pass
            // it into this function?
            return;
        }

        [self sortContents];
        [self performLayout];

        // we must remove observer from within this block, because
        // nothing else has a reference to it otherwise.
        [[NSNotificationCenter defaultCenter] removeObserver: tmpObserver];

        // see above-- how do I re-register to keep receiving the
        // events?
    }];
}

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:

- (void) onlyFlawlessWayOfDoingIt
{
    typedef void (^notificationObserver_block)(NSNotification *);

    // our observer block needs to reference itself, which means we
    // need a variable to hold it which is defined up front. For
    // clarity and safety (and sanity) we'll make it a __block
    // variable:
    __block notificationObserver_block observer_block = (notificationObserver_block)0;

    // the observer needs to be assigned by the observer block when it
    // signs up again, which means another __block variable:
    __block id tmpObserver = nil;

    // the observer block gets defined and assigned to its variable
    // here:
    observer_block = ^(NSNotification * note) {
        if ( [self validateContents] )
        {
            [self sortContents];
            [self performLayout];
        }

        // remove the existing observer
        [[NSNotificationCenter defaultCenter] removeObserver: tmpObserver];

        // signup again, because we want to keep receiving this
        // notification
        tmpObserver = [[NSNotificationCenter defaultCenter]
                       addObserverForName: @"TheNotification"
                                   object: nil
                                    queue: [NSOperationQueue mainQueue]
                               usingBlock: observer_block]; // references currently-executing block
    };

    // now we need to repeat the last line of our defined block to
    // start the ball rolling:
    tmpObserver = [[NSNotificationCenter defaultCenter]
                   addObserverForName: @"TheNotification"
                               object: nil
                                queue: [NSOperationQueue mainQueue]
                           usingBlock: observer_block];
}

Phew.

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 __block variable for good measure (and the sake of my continued sanity).
  • We also need to be able to reassign the (initially on-stack) tmpObserver object from within this block, so we need to define that as a __block variable too.
  • 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 -removeObserver: and addObserverForName:... methods? Will it get missed? Why, yes, it will.

**Sigh**.

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 -main or -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 in -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 place.

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.

Footnote:

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.**


© 2009-2019. All rights reserved.

Powered by Hydejack v9.1.6