Playing with NSTimer

It’s been a long while. Part of it is that I’ve been busy working on Hazel 3. And since I’m not interested in writing a book, I’m finding it hard to be motivated to keep posting here. Nonetheless, every so often I have to let something squirt out.

Today, we are going to talk about NSTimer. One thing you may or may not have noticed is that in certain cases where time is suspended (putting your machine to sleep) or altered (changing the clock), NSTimer has a tendency to try and compensate. For instance, say you set a timer to fire in an hour. You close the lid on your MBP and come back 30 minutes later. You’ll see that the fire date for the timer has adjusted to take into account the time it was asleep. Here’s a quickie diagram for those not quite following:

timer-timeline.png

Now, this is great if you wanted a timer to fire an hour later in the machine’s conception of time. But there are times when you want it to fire on the actual date you set on it. Typical example is setting a timer for a calendar appointment. That time is an absolute point in the user’s timeline and you don’t want that timer to shift to compensate for the machine’s timeline.
The basic fix here is when one of these time-altering events occurs, you reset the fire time on the timer to the original time that was set on it when it was created. Luckily, there are notifications for when the machine wakes from sleep as well as when the system clock is changed (this one being new in 10.6). Naturally, one would think to make an NSTimer subclass as the most straightforward way to do this. Unfortunately, it seems that it doesn’t work. Even if you get over the hurdle that NSTimer is actually abstract (like a class cluster), you have to add the timer to the run loop to schedule it. While NSRunLoop seems to accept the NSTimer subclass, it doesn’t ever fire. I don’t know if NSRunLoop is looking for the NSTimer class specifically or if it’s calling some private method that I need to override. If someone else has had any luck doing this, drop me a line.

The alternative is a category. The problem is that we need at least one ivar to store the original fire date. Normally, you can’t add ivars in a category but in this glorious post-10.6 age, we can, via associative references. It’s basically just a dictionary where you can store variables associated with an object. Sure, you could have implemented that yourself but the nice thing is that it handles memory management as well. When the object is dealloc’ed/collected, depending on the memory management you specify, it can also release/collect any associated objects.

So, NSTimer category it is. It supplies the method (among others) +scheduledTimerWithAbsoluteFireDate:target:selector:userInfo: which creates and returns an autoreleased and scheduled timer that will fire on the given date regardless of any time fluctuations/lapses. If all of a sudden the fire date has passed (like if the machine was asleep during the fire date or if the system clock was set ahead), it will fire immediately. Note that calling -setFireDate: won’t quite work in the sense that if the timer ever has to be reset from a time shift, it will be set to the date used when creating the timer, not the one that you set it to afterwards. This is an implementation limitation as I am using a category and can’t override methods (at least not without doing some method-swizzling which is not something I want to deal with). In cases where you need to change the fire date, I’d suggest just creating a new NSTimer.

I’ve also included a test harness where you can see it in action compared against a timer running in normal (non-absolute) mode. When run normally, both timers should fire at the same time but you can test things out by changing the system clock or putting your machine to sleep. You’ll see that the regular timer’s fire date will start to drift out while the “absolute” timer will stick with the time originally set.

I also went ahead and added methods to have NSTimer to use blocks instead of a target/selector combo. Note sure why this wasn’t in there already but I thought it’d be useful.

And that’s not all! Included is my NoodleGlue class. It’s a simple little class that just wraps a block (plus another block for cleanup, if you need it). It’s useful for cases where you want to set a target and selector for some object to use but don’t want to create a new class or method for it. Check out the source code for the NSTimer category to see how its used both with NSTimer (to implement the block API) as well as NSNotificationCenter. In the latter case, there is a blocks-based API but I had special memory management requirements that couldn’t be done with the existing API.

Needless to say, this extension only works on 10.6+. It’s in the latest version of NoodleKit and as a result I’ve made NoodleKit require 10.6 as well. Just build and run the TimerLab target. Fixes and suggestions welcome.

[Update 8:35pm EDT] I neglected to push the code to the github repository. Sorry about that. Everything should be there now.

Category: Cocoa, Downloads, OS X, Programming 7 comments »

7 Responses to “Playing with NSTimer”

  1. Jean-Daniel

    If this is for 10.6, you may try to use Grand Central Dispatch timer instead. I don’t know if they suffer the same issue though. But as GCD does not store relative and absolute time (walltime) the same way, it should works out of the box.

  2. mr_noodle

    Interesting notion. Doing a quick test specifying the time both using dispatch_time and dispatch_walltime, I get the same result (the timer delays itself when the machine is put to sleep). Thanks for the suggestion though.

  3. Joel N

    Hazel 3!? I know its still work in progress, but what’s the target release date?

  4. mr_noodle

    Sorry about the late response. I was on vacation when you commented and forgot about it when I got back.

    No target release date. I am working on it though progress is slow at the moment for various reasons. I suggest keeping an eye on the forums or twitter for any updates on this front. I’ll definitely be doing some sort of beta at some point.

  5. Adam

    Thanks for this blog post. I’ve been having the same issue on iOS and I’m glad to hear it’s a behavior others have noticed and documented. I’ll be building a workaround based on your NSTimer category.

  6. JongAm Park

    I noticed this sentence in “Concurrency Programming Guide” (Dispatch Sources).

    “When a computer goes to sleep, all timer dispatch sources are suspended. When the computer wakes up, those timer dispatch sources are automatically woken up as well. Depending on the configuration of the timer, pauses of this nature may affect when your timer fires next. If you set up your timer dispatch source using the dispatch_time function or the DISPATCH_TIME_NOW constant, the timer dispatch source uses the default system clock to determine when to fire. However, the default clock does not advance while the computer is asleep. By contrast, when you set up your timer dispatch source using the dispatch_walltime function, the timer dispatch source tracks its firing time to the wall clock time. This latter option is typically appropriate for timers whose firing interval is relatively large because it prevents there from being too much drift between event times.”

    So, probably dispatch_walltime can solve it…

  7. JongAm Park

    Umm… please dismiss my reply. It doesn’t seem to work.
    This timer is scheduled to be fired in every 5 secs.

    2012-10-16 13:59:37.246 NSOperationTest[1573:303] timer fired..
    2012-10-16 13:59:42.247 NSOperationTest[1573:303] timer fired..

    2012-10-16 13:59:58.098 NSOperationTest[1573:303] timer fired..
    2012-10-16 14:00:02.247 NSOperationTest[1573:303] timer fired..


Leave a Reply



Back to top