ARC obliterated entire classes of bugs by automating memory management in acyclic contexts and providing zeroing weak references. Thanks to this, many classes no longer need to perform any cleanup during object destruction. The most common reason for dealloc these days? Notifications.

Broadcast messaging is vital to today's applications, but NSNotificationCenter has resisted modernization. Among its modern failings, its API enables fragile usage and it stores unsafe references to observers.

Karl and I were discussing the error-prone nature of APIs that require bracketed calls, like mutexes that conform to NSLocking1 and addObserver:selector:name:object:/removeObserver:name:object:. He posed the question: how do we get rid of bracketing APIs? To start with, how can we make NSNotificationCenter support weak observers?

The answer, of course, is a proxy whose lifetime is tied to the observer using object association:

The only API the proxy needs to provide is an initializer:

@interface NNWeakObserverProxy : NSProxy

- (instancetype)initWithTarget:(NSObject *)target
                      selector:(SEL)selector
                          name:(NSString *)name
                        object:(id)sender
       addToNotificationCenter:(NSNotificationCenter *)notificationCenter;

@end

The implementation is pretty simple: the proxy adds and removes itself from the notification center in its own initialization methods, the target is stored weakly so in the time between target and proxy destruction the message is safely dispatched to nil, and the message forwarding machinery is minimal.

@interface NNWeakObserverProxy ()

@property (nonatomic, readonly, weak) NSObject *target;
@property (nonatomic, readonly, assign) SEL selector;
@property (nonatomic, readonly, strong) NSString *name;
@property (nonatomic, readonly, strong) id sender;
@property (nonatomic, readonly, strong) NSNotificationCenter *notificationCenter;

@end


@implementation NNWeakObserverProxy

- (instancetype)initWithTarget:(NSObject *)target
                      selector:(SEL)selector
                          name:(NSString *)name
                        object:(id)sender
       addToNotificationCenter:(NSNotificationCenter *)notificationCenter;
{
    _target = target;
    _selector = selector;
    _name = name;
    _sender = sender;
    _notificationCenter = notificationCenter;
    
    [_notificationCenter addObserver:self selector:_selector name:_name object:_sender];
    
    return self;
}

- (void)dealloc;
{
    [_notificationCenter removeObserver:self name:_name object:_sender];
}

#pragma mark Message forwarding

- (id)forwardingTargetForSelector:(SEL)aSelector;
{
    if (aSelector == self.selector) {
        return self.target;
    }
    
    return self;
}

@end

Lastly, a method in a category on NSNotificationCenter to tie it all together. Its primary responsibility is encapsulation, ensuring that the proxy's lifetime is tied as closely to the lifetime of the observer as possible:

- (void)nn_addWeakObserver:(id)observer selector:(SEL)selector name:(NSString *)name object:(id)sender;
{
    NNWeakObserverProxy *proxy = [[NNWeakObserverProxy alloc] initWithTarget:observer
                                                                    selector:selector
                                                                        name:name
                                                                      object:sender
                                                     addToNotificationCenter:self];
    objc_setAssociatedObject(observer, (__bridge const void *)proxy, proxy, OBJC_ASSOCIATION_RETAIN);
}

This was simplified to fit in a blog post so it has a few limitations, such as the inability to remove the observation without destroying the observer itself, but it offers a glimpse into a future topic. Stay tuned!

edited on wednesday april 9th, 2014 at 13:33 (PDT):

Dave Lee pointed out that the proxy can use forwardingTargetForSelector: instead of forwardInvocation:. This is not only much faster, but also eliminates the need to cache the method signature. The code has been updated to take advantage of this. Thanks Dave!

1 Less error-prone approaches for mutexes include @synchronized and the modern APIs that take blocks