Notifications part 3 - Gotchas

Mark Dalrymple's Headshot
Mark Dalrymple Cocoa iOS

Threading

One thing that catches some notification users unaware is that you’re not guaranteed of which thread your notification handler is run on (with one exception). When a notification is posted, the notification center chugs through its list of observers, figures out if an observer should fire, and then uses the selector to call the appropriate method (or invoke the appropriate block). The notification posting finishes when it runs out of observers. Then the thread can continue on with other work. If you post a notification on a background thread, the observers are called on that background thread. This is the same behavior with KVO, BTW.

What does this mean for you? The most important thing to worry about is UI-related stuff happening on threads other than the main thread. This can be easier than you think. Consider some code that happily adds an observer:

    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

    [center addObserver: self
            selector: @selector(handleModelChange:)
            name: kGroovyModelContentsChanged
            object: nil];

That’s cool. It handles a change to the model.

Pretend the model handles large amounts of data, so it does stuff in the background:

dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self.model load];
    [self.model process];
    [self.model frobulate];

    // All done!  Send out a notification!
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center postNotificationName: kGroovyModelContentsChanged
            object: self.model];
});

But what about the notification handler? What if it touches the UI?

- (void) handleModelChange: (NSNotification *) notification {
    [self.button setTitle: @"Ready To Rock"];
} // handleModelChange

Oops. It’s now touching AppKit off of the main thread. This can be the source of subtle (and sometimes not-so-subtle) bugs. Many notifications, especially ones that come from Cocoa, will come through on the main thread. You’re OK. Be careful when you post your own notifications - either schedule the notification posting on the main thread, or make sure through design or coding conventions that any notification handlers are able to be run on any thread.

What about that “with one exception”? Recall the new-school notification syntax which takes a block and a notification queue. You can use the main queue (by passing [NSOperationQueue mainQueue]). In this case the notification block will always be run on the main queue, no matter which thread posted the notification.

Over-aggressive Unregistration

removeObserver is a pretty handy call. It removes the object from the notification center altogether. This is generally safe to call in dealloc, but be very careful if you use the blanket unregistration anywhere else. You might be tempted to write this code. (I know I was tempted to write this code.)

// User turned off iCloud, so we don't need to handle the
// NSUbiquitousKeyValueStoreDidChangeExternallyNotification anymore
[center removeObserver: self];

It’s like a sledgehammer or a Sawzall - a useful tool but can do a lot of collateral damage. Cocoa may have registered notifications on your behalf, perhaps some memory handling notifications in iOS, keyboard handling notifications, device rotation notifications, etc. You might have subclasses that are expecting notifications, and now won’t get any more. It’s a good practice for your code to explicitly remove itself for each notification name that it adds an observer for. It can get tedious if you have a lot of notifications you’re listening to, but it’s safer.

It’s not a problem with block-based observers because you get the token object back, one for each observer you register. You then call removeObserver with each of the observer objects.

Dangling Pointers

NSNotificationCenter does not retain (or keep a strong reference under ARC) the object you register with one of the addObserver: methods. This means you’re responsible for removing the object from the notification center before it dies. dealloc is a good catch-all place to do this. If possible, you should remove yourself from the notification center as soon as you know that handling a notification is no longer necessary.

Retain Cycles

It’s possible to get retain cycles when using the block-based notification NSNotificationCenter API. This isn’t a new retain cycle opportunity, but one that happens with any block-based API where the block has the same lifespan as the object. I’ll have more on this in a future posting, but I’ve written about it before during my other life. In short, a block retains any object it captures as read-only, including self:

- (id) initWithName: (NSString *) name {
    if ((self = [super init])) {
         NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        _notificationToken = 
            [center addObserverForName: @"SomeNotification"
                                object: nil
                                 queue: [NSOperationQueue mainQueue]
                            usingBlock: ^(NSNotification *notification) {
                NSLog (@"%@ got a notification!", self.name);
            }];
    }
    return self;
} // init

Notice that self is explicitly referenced inside of the block. self can also be referenced implicitly by accessing an instance variable. By default, blocks capture variables read-only, and objects are retained. Therefore self is retained, and we’re in a retain cycle situation.

With manual memory management, you avoid the cycle by making a local “self” that is a block storage class:

...
        <strong>__block BNRLeakyThing *blockSelf = self;</strong>
        _notificationToken = 
            [center addObserverForName: @"SomeNotification"
                                object: nil
                                 queue: nil
                            usingBlock: ^(NSNotification *notification) {
                 NSLog (@"%@ got a notification!", <strong>blockSelf</strong>.name]);
             }];
...

And ARC-styles, you make a weak reference to self:

...
        <strong>__weak BNRLeakyThing *weakSelf = self;</strong>
        _notificationToken = 
            [center addObserverForName: @"SomeNotification"
                                object: nil
                                 queue: nil
                            usingBlock: ^(NSNotification *notification) {
                 NSLog (@"%@ got a notification!", [<strong>weakSelf</strong> name]);
             }];
...

Deadlock and Seizures

Finally, a bit about design. A reasonable application architecture is having your objects being very decoupled, with most of your inter-object interactions happening with notifications. Back in the mists of time I once worked on a publishing tool that plugged into a host application, and we implemented the tool mainly as notifications. Something interesting would happen, like a mouse click, which would generate a notification. Then some other object, say a tree view, would respond by expanding itself, which would send out notifications about each object that was revealed.

It was kind of cool, allowing us to easily add and remove functionality just by adjusting whether an object listened for a particular notification. We ran into two problems, though. One was deadlock. Object Jeff broadcasts something that’s handled by object Sheri, and in the course of handling that notification Sheri broadcasts something that Jeff is listening to, which triggers another notification that Sheri might use, and back and forth. They were so busy talking to each other that the application never returned to the event loop, and was effectively locked up. This is possible to do in Cocoa as well:

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

[center addObserverForName: @"Pony Boy"
        object: nil
        queue: nil
        usingBlock: ^(NSNotification *notification) {
            NSLog (@"handling pony boy notification");
            [center postNotificationName: @"My Little Pony Boy"
                    object: nil];
        }];

[center addObserverForName: @"My Little Pony Boy"
        object: nil
        queue: nil
        usingBlock: ^(NSNotification *notification) {
            NSLog (@"handling my little pony boy notification");
            [center postNotificationName: @"Pony Boy"
                    object: nil];
        }];

[center postNotificationName: @"Pony Boy"
        object: nil];

You can put this into a little command-line app, and watch it loop until it runs out of stack space and crashes.

A similar problem can cause performance issues. A triggering event can cause a huge cascade of notifications triggering other notifications, burning a lot of CPU while it handles the work. This effect can be likened to a an epileptic seizure, where masses of electrical signals overloads the brain. In our editor, we had performance problems about mid-development cycle, once we reached a critical mass of functionality and data we were processing. Fundamentally we were just doing too much work, sometimes doing the same work over and over again as we were triggered by different notifications.

We fixed it by having our notification handlers not post any notifications themselves. This solved the deadlock problem. To solve the seizure problem, when an object was notified about something, it put itself on a queue to do the work. The queues were coalescing, meaning that if object Jeff put itself on the queue five times, it would actually only be on the queue once. Cocoa’s NSNotificationQueue can do this kind of stuff for you.

[center postNotificationName: @”All Done!”];

That wraps up this tour of NSNotification for now. A very useful tool with an easy-to-use API, with only a couple of sharp corners.

Recent Comments

comments powered by Disqus