Improving Notification Center

Originally published on Macoscope's Blog.

A couple of weeks ago, in his How Not to Crash series, Brent Simmons wrote in detail about common issues related to using NSNotificationCenter. The piece clearly demonstrated how much busy work rests on our shoulders and how hard it is to get everything right.

I wanted to make NSNotificationCenter easier and less error-prone, by fixing these three common sources of bugs:

  • block-based API is unsafe (non-obvious retain cycles) and hard to debug
  • it's possible to register twice for the same notification
  • there's no automatic unregistration in -dealloc (except for iOS 9 or newer, but only for selector-based notifications)

To do that I created a thin layer that sits between your code and NSNotificationCenter. It's called NotificationController. You can find the source on GitHub here.

Let me show you how I approached each of these problems.

Safer Block-based API

Nearly two years ago Drew Crawford wrote an epic piece called NSNotificationCenter with blocks considered harmful. He needed 7(!) attempts to get notification center with blocks to work correctly. One passage really stood out to me:

Hey, you want to know what else is scary? This code builds cleanly. Not a peep from the compiler; not a peep from Clang Static Analyzer. In fact, every buggy code listing you see in this post gets a clean bill of health from both. This is in spite of the fact that LLVM has a warning for this very bug. You might have seen it:

Capturing 'self' strongly in this block is likely to lead to a retain cycle

Clang is just not powerful enough, in its present form, to find this type of bug. Consider yourself alarmed.

I wanted to learn how Clang decides when to show a warning, so I did what any reasonable person would do and dove into its source. Retain cycle checking is performed during the semantic analysis phase. Sema::checkRetainCycles is an overloaded method responsible for these checks. It performs its analysis for three cases:

  • message send
  • property assignment
  • variable assignment

In case of a message send, the compiler checks for retain cycles only when a selector looks like a setter, i.e., it starts with the word add or set. This check is rather simplistic. A warning isn't presented to the user when using NSNotificationCenter, because the compiler doesn't know the lifecycle of that object.

(As a side note, there's an interesting edge case in isSetterLikeSelector function – selectors starting with addOperationWithBlock (like in NSOperationQueue) are ignored, so you won't see any retain cycle warnings for them.)

Knowing all this there were two ways to proceed:

  1. Add special handling for retain cycles in the compiler for when -addObserverForName:object:queue:usingBlock: is called on an NSNotificationCenter instance. Submitting patches to Clang takes a lot of time and the implementation would probably be hard because of the dynamic nature of Objective-C.
  2. Reorganize relations between objects, so that the compiler can show warnings using already implemented infrastructure.

I decided to go with the second option as it seemed to be a better tradeoff between gains and time needed. Header of the first version of MCSNotificationController looked like this:

@interface MCSNotificationController : NSObject

- (instancetype)initWithObserver:(id)observer;
- (BOOL)addObserverForName:(nullable NSString *)name
                    sender:(nullable id)sender
                     queue:(nullable NSOperationQueue *)queue
                usingBlock:(void (^)(NSNotification *note))block;

@end

Drew's example translated to NotificationController's API looks like this (full test on GitHub):

@interface YourAttempt : NSObject

@property (nonatomic, assign) NSInteger localCounter;
@property (nonatomic, strong) MCSNotificationController *notificationController;

@end

@implementation YourAttempt

- (instancetype)init
{
  if (self = [super init]) {
    _notificationController = [[MCSNotificationController alloc] initWithObserver:self];

    [_notificationController addObserverForName:notificationName sender:nil queue:nil usingBlock:^(NSNotification *note) {
      NSInteger oldCounterValue = globalCounter;
      globalCounter++;
      self.localCounter++;
      NSAssert(globalCounter == oldCounterValue+1, @"Atomicity guarantee violated.");
    }];
  }

  return self;
}

@end

The compiler has no problems with finding retain cycles here, because it's clear to it what the graph of objects look like. We get warnings on both direct and indirect (e.g., ivar or NSAssert) self access. It's a pretty significant improvement, because we get to know about issues at compile-time.

There's also a nifty category on NSObject that creates a lazy-loaded mcs_notificationController property for you. It simplifies things even further, what you're left with is just one message send to -addObserverForName:sender:queue:usingBlock: (inspired by KVOController).

Preventing Duplicate Registrations

NSNotificationCenter doesn't prevent you from registering for a given notification more than once and it's a really easy mistake to make (for example when registering/unregistering in one of -viewWillAppear:, -viewDidAppear:, -viewWillDisappear:, and -viewDidDisappear: methods). These bugs are generally hard to debug, because handlers are almost always written without thinking how they'd behave when called repeatedly.

Both -addObserverForName:sender:queue:usingBlock: and -addObserverForName:sender:selector: in MCSNotificationController prevent you from making this mistake. First, they check whether you're already registered and don't do anything if you are. Then, they both return a BOOL value indicating whether registration was successful.

Automatic Unregistration in dealloc

A couple of weeks before WWDC 2015, I actually thought that it would be great to not have to manually unregister from notifications in -dealloc. To my surprise, that's exactly what Apple introduced in iOS 9 and OS X 10.11. This new feature doesn't work with the block-based API, though:

Block based observers via the -[NSNotificationCenter addObserverForName:object:queue:usingBlock] method still need to be un-registered when no longer in use since the system still holds a strong reference to these observers.

NotificationController, on the other hand, gives you automatic unregistration when using both block- and selector-based APIs on iOS 7-9.

Conclusion

NotificationController tries to solve some real-life problems associated with NSNotificationCenter usage. Its API is as similar to NSNotificationCenter as possible, so it shouldn't put any additional mental burden on you (as it is yet another external dependency).

The API is compatible with Swift, but as Swift is still a work in progress, you won't see any warnings about retain cycles, because they're simply not implemented in the Swift compiler. We hope this fact changes soon.