Subscriptions and locks commonly come with APIs that require balanced calls, but they represent a subclass of a larger pattern of managing resources that must be freed. In the case of global notifications, the observer causes system resources to be allocated for notification dispatch, and that all needs to be cleaned up when the observer is no longer interested.

Another pattern of resource allocation is when dealing with file descriptors, which are easy to forget to close in sufficiently complicated codebases. A solution to this problem is an idiom known as Resource Acquisition Is Initialization, which NSFileHandle implements1. There are still some areas where this paradigm isn't present in Apple's APIs, such as NSDocument and NSMachPort, but today we're going to look at NSTimer.

From the perspective of RAII, NSTimer is a disaster. Instead of maintaining an unsafe reference to its target like NSNotificationCenter, an instance of NSTimer retains its target until it's invalidated. Furthermore, the timer itself is retained by the run loop to which it is registered. Since a reference to the timer must be maintained in order to manually invalidate it, the resulting object graph is strong back references plus a retain cycle:

Since NSTimer is responsible for allocating scheduler resources on behalf of its target, and anything involving the run loop should be an implementation detail of the timer itself, the object graph from the perspective of the client should look like this:

We could solve this problem with a proxy that stands in for the target to avoid the reference cycle, plus another proxy that stands in for the timer and invalidates it in the pattern of NNSelfInvalidatingObject. The resulting relationship graph would look like:

This still sucks, and doesn't change the fact that NSTimer also has awkward API issues. Clients shouldn't have to know about run loops in order to be called after a delay2 or on a periodic basis. In fact, there's a lot that can be safely removed from NSTimer's API:

  • invalidate: Even though the timer won't necessarily be destroyed immediately, it has to be retained somewhere during its lifetime and checking if (self.timer != timer) { return; } is already a common and safe pattern for target-action style method invocations in Objective-C.
  • isValid: It's tempting to argue that non-repeating timers need this API, but its purpose in NSTimer is mainly to ensure that the timer isn't retaining the target anymore. The purpose of non-repeating timers is to cause side effects. One of those side effects should be to dispose of the timer.
  • Non-repeating timers: If a timer should only fire once, the method that it invokes can clean it up. If it's important that a timer existed in the past and did fire, then it's best to admit the extra bit of state as a property than to prolong the in-memory lifetime of an invalid, useless object.
  • Run loops: Timers shouldn't make threading guarantees. Since we're not in a perfect world, a good compromise is to offer the option for the timer to fire on either the main thread, or on a background thread.
  • NSInvocation: NSInvocation is an implementation detail of NSTimer and long-lived invocations are the root of its strong reference issues, since they only store argument and target references either strongly or unsafely. We could rewrite it to reference its target weakly, but invocations aren't used often enough to be worth it—NSTimer should support blocks instead.
  • fireDate: NSTimer supports this for irregular timers, and the same mechanism can be achieved by making the time interval a mutable property of the timer, further reducing the scope of the API.

These changes leave a simple new API that easily replaces NSTimer:

@interface NNTimer : NSObject

+ (NNTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
                                      block:(dispatch_block_t)block
                                      queue:(dispatch_queue_t)queue;
+ (NNTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
                                     target:(id)target
                                   selector:(SEL)aSelector
                                   userInfo:(id)userInfo
                                      queue:(dispatch_queue_t)queue;

@property (nonatomic, assign, readwrite) NSTimeInterval timeInterval;
@property (nonatomic, strong, readwrite) id userInfo;

- (void)fire;

@end

GCD and weak references didn't exist when NSTimer was created, so the resulting implementation is super simple:

#import "NNTimer.h"

@interface NNTimer ()

@property (nonatomic, readwrite, assign) NSUInteger mutationCounter;
@property (nonatomic, readonly, strong) dispatch_queue_t queue;
@property (nonatomic, readonly, strong) dispatch_block_t job;

- (void)_enqueueNextJob;

@end

@implementation NNTimer

+ (NNTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
                                      block:(dispatch_block_t)block
                                      queue:(dispatch_queue_t)queue;
{
    NNTimer *result = [NNTimer new];
    
    result->_timeInterval = seconds;
    result->_job = block;
    result->_queue = queue;
    [result _enqueueNextJob];
    return result;
}

+ (NNTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
                                     target:(id)target
                                   selector:(SEL)aSelector
                                   userInfo:(id)userInfo
                                      queue:(dispatch_queue_t)queue;
{
    NNTimer *result = [self scheduledTimerWithTimeInterval:seconds
                                                     block:nil
                                                     queue:queue];
    
    __weak id weakTarget = target;
    __weak NNTimer *weakTimer = result;
    result->_job = ^{
        __strong id strongTarget = weakTarget;
        __strong NNTimer *strongTimer = weakTimer;
        if (!strongTarget || !strongTimer) { return; }
        
        // Calling the IMP directly instead of using performSelector: or
        // performSelector:withObject: in order to safely shut up the compiler
        // warning about potentially-leaked objects.
        IMP imp = [strongTarget methodForSelector:aSelector];
        void (*imp1)(id, SEL, id) = (void *)imp;
        imp1(strongTarget, aSelector, strongTimer);
    };
    
    result->_userInfo = userInfo;
    
    return result;
}

- (void)setTimeInterval:(NSTimeInterval)timeInterval;
{
    self->_timeInterval = timeInterval;
    self.mutationCounter++;
    [self _enqueueNextJob];
}

// -fire makes two mutually-exclusive promises. One is that the job is executed
// synchronously, the other, less-obvious, promise is that the job is run on
// the same queue (or in NSTimer's case, run loop) as the scheduled firings.
// NSTimer opts for the simplest solution of keeping the synchronicity
// guarantee, and so does NNTimer.
- (void)fire;
{
    self.job();
}

- (void)_enqueueNextJob;
{
    NSTimeInterval nonNegativeTimeInterval = self.timeInterval >= 0.
                                           ? self.timeInterval : 0.;
    int64_t delta = nonNegativeTimeInterval * NSEC_PER_SEC;
    dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, delta);
    
    NSUInteger mutationsAtSchedulingTime = self.mutationCounter;
    __weak __typeof(self) weakSelf = self;
    dispatch_block_t block = ^{
        __strong __typeof(self) strongSelf = weakSelf;
        
        if (!strongSelf) { return; }
        if (mutationsAtSchedulingTime != strongSelf.mutationCounter) { return; }
        
        [strongSelf fire];
        [strongSelf _enqueueNextJob];
    };
    
    dispatch_after(delay, self.queue, block);
}

@end

Something shaped very much like this is going to land in NNKit soon. Stay tuned!


1 NSFileHandle also implements close, the use of which leaves a useless dead object. If you find yourself using close, you have architectural problems.
2 Before anyone says it, if you don't need invalidation, such as with delayed initialization, just use dispatch_after.