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:
GCD and weak references didn't exist when NSTimer was created, so the resulting implementation is super simple:
Something shaped very much like this is going to land in NNKit soon. Stay tuned!