The secret world of NSTimer
Tips & tricks of NSTimer and how to create a robust alternative using GCD
› This article is also available on my blog at this link.
Timer allows us to execute some piece of code after a timer interval one or more times.
There are multiple types of clocks used to create timers and, even if apparently all of them run at the same rate, they still have different behaviours.
We can list the following list of timer types:
- Real-time clock or RTC. Its a computer clock (usually in the form of an integrated circuit) that keeps track of the current time; user can change this clock arbitrarily and the NTP (Network Time Protocol) do it best to keep it in sync with an external reference. Its value increases by one second per real second but sometimes it may run faster/slower or jump forward (thanks to Gavin Eadie which pointing me to the fact that the clock never runs backwards.In the event of an NTP sync discovering that the clock is running fast, it should be slowed until “real time” catches up) when it attempts to sync with the external source.
- Monotonic Timer. Its a counter incremented by a physical signal sent to the CPU using an timer interrupt. On Apple platforms this value is returned by the Mach kernel via mach_absolute_time() . Returned value is dependent by the CPU so you can’t just multiply it by a constant to get a real world value; rather, you should call a system-provided conversion function to convert it to a real world value (CoreAnimation has a convenient method for that: CACurrentMediaTime() ). The fact it gets reset on boot make it not so interesting to get how much time has elapsed in real world, however its the most precise way to measure difference between two intervals.
- Boot Timer. It’s just a special Monotonic Timer which does not pause when the system goes to sleep; the most common way to get its value is to call the
uptimefunction from terminal.
On Apple platforms the most common way to create timers is by using the NSTimer class; in fact its just a wrapper around a monotonic timer.
For this reason using NSTimer may end with an unpredictably behaviour, especially on iOS, where the opportunistic use of resources may ends in some edge cases as described above.
How NSTimer works
In order to fully understand NSTimer we need to talk a bit about NSRunLoop ; once launched each application create a first NSThread , called Main Thread; each Thread has an associate Run loop that manages input sources such as mouse, keyboard, touches, connections… and obviously our timers.
You can think of RunLoop as a postbox that waits for new messages and delivers them to the appropriate recipients: its basically a messaging mechanism, used for async or inter-thread communication.
Some platforms — like Windows — call it Message Pump but the inner concept still the same.
In fact Run loops represent the main difference between command-line and interactive (usually UI-based) applications.
While the first ones are launched with params, execute their stuff, then exit, an interactive app wait for user input, react to it and gets waiting again.
There is exactly one Run loop per thread; a run loop is composed by a collection of input sources (keyboard, touches etc.) to be monitored and a collection of observers to be notified.
Implicitly or explicitly a Run loop is initialized with a particular mode in which to run; during its life only sources associated with that mode are monitored and allowed to deliver their events; only observer associated with that mode are notified of new data.
Cocoa/UIKit defines several type of modes: on iOS there is a special mode called UITrackingRunLoopMode : it set while tracking in controls takes place. It’s a fundamental piece because it allows UI events to be presented smoothly. For example in drag loop or other user interface tracking loops in this mode, processing in this mode will limit the input event. For example, when the finger UITableView drag will in this mode.
While the main thread’s run loop is in UITrackingRunLoopMode , most background events like network callbacks, aren’t delivered. and no extra processing is done (and it means no lag during scrolling).
In this continuous loop to check events, NSRunLoop also check for timer interval elapsed event; once detected it will call method registered by the NSTimer class.
What it means for NSTimers ? It’s simple; usually they don’t fire when the user is scrolling a table or other UIScrollView — or doing anything else that puts the run loop in event tracking mode
The no-no of NSTimer
Even if it’s okay to use NSTimer I would to get a robust alternative to avoid at least some of the main limitations of using this class.
No pure Swift Classes
While this is not certainly an issue, we want to have a Timer which is not dependant fromNSObject and Obj-C where we can set an event callback without worrying about retain cycles.
No support for real-time
This mean NSTimer cannot work as real-time timers: the point of the loop where fire events are managed might get delayed and you can loss precision.
In fact both NSTimer and GCD timers are not suitable for realtime need (you cannot use them for latency-sensitive purposes, like video buffer sync or audio processing); the effective resolution of the time interval for a timer is limited to on the order of 50–100 milliseconds (hint: CADisplayLink).
Requires a valid Run Loop
As you seen, NSTimer requires an active run loop; when initialized in Main Thread it automatically uses the main run loop. If you need to make a background timer you need attach it to the thread’s run loop and invoke run() to make it active.
Retain Cycles & Threads issues
You must also pay attention to invalidate() ; you must remember to call it otherwise Run loop keeps a strong reference to the timer’s target object causing possible leaks.
Moreover invalidate() must be called in the same thread where you have created the timer itself.
Cannot be reused
Another pitfall is you cannot reuse an invalidated timer instance; you are forced to allocated a new instance instead. No pause, no resume.
A new Timer with Grand Central Dispatch
Grand Central Dispatch offers an easy alternative to create timer on Apple platforms: DispatchSourceTimer .
Our scope is to use it to make a new Timer class for our projects; we’ll call it Repeat . The main goals of this small project are:
- Simple, less verbose APIs methods to create and manage our timer.
- Avoid strong reference to the destination target and avoid NSObject inheritance.
- Use callback to inform about fire events; allows multiple observers to subscribe to the same timer
- Ability to pause , start , resume and reset our timer.
- Ability to set different repeat modes ( infinite : infinite sequence of fires, at regular intervals,finite : a finite sequence of fires, at regular intervals, once : a single fire events at specified interval since start).
First of all we want do define an easy way to indicate the interval of the timer; we could keep DispatchTimeInterval but we also want to be able to set a floating value for seconds while the original class uses only Int values.
Our Repeat.Interval is just an enum with a value property which return the correct DispatchInterval and allows us to specify a value of a time unit:
The core function of our class is the configureTimer() function where we create our DispatchSourceTimer :
The following code just create a new
DispatchSourceTimer attach it to a given
DispatchQueue (or a newly created one), set the repeat behaviour and setup a callback to receive (notice the
[weak self]) fire events.
In order to accept multiple observer we’ve defined a Dictionary where we store each new observer function along with a given unique identifier. Once fired the dictionary is enumerated and the event is dispatched to any registered observer.
The unique is can be also used to remove an existing observer.
The following methods allows to register/unregister observers:
We also added pause, start/resume and reset functions.
As we said GCD timers can be somewhat sensitive.
If you try and resume/suspend an already resumed/suspended timer, you will get a crash with an error like “Over-resume of an object”. To solve it we just need to balance the calls to suspend and resume. Each method has a safe wrapper to avoid multiple calls to the same state.
Keep in mind: we should also ensure its accessed from the same thread/queue otherwise we will need to add an internal serial queue to prevent race conditions.
As you can see
deinit() function is a bit special: we need ensure it can be deallocated properly by cancelling the timer instance otherwise it will call a deallocated object.
Also we want avoid cancelling a timer that has been suspended because this will also trigger a crash.
Finally we want to expose some init shortcuts like:
every(_ interval: Interval, count: Int? = nil, handler: @escaping Observer)
once(after interval: Interval, _ observer: @escaping Observer)
to create our timer easily as:
We can also add additional observers:
And that’s all!
As usual the full code along with unit tests for Repeat is available on my github account. Repeat is compatible with all Apple’s platform and you can install it with carthage, cocoapods or directly by dragging the single source code file of the project.
All contributions to this text and the project are welcomed.