nstimer for fun and profit
Let’s talk about NSTimer
. It’s a core class that every Apple developer needs to use. And yet, despite that, it’s
still misused regularly. We’ll start with the basics.
Using a NSTimer
is relatively easy. You just need to create an NSTimer
object and make sure it’s added to the runloop
you want it to fire on. Of course, it’s a little more difficult than that because Apple provides multiple
ways to go about this task. In addition, Apple also provides two ways to get notified when the timer fires. Let’s
take a closer look at the three ways we have of creating an NSTimer
.
scheduledTimerWithTimeInterval:invocation:repeats:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
These are the easiest of the three ways of creating a NSTimer
. You simply call this method and your timer will be allocated, initialized and scheduled on the current run loop.
timerWithTimeInterval:invocation:repeats:
timerWithTimeInterval:target:selector:userInfo:repeats:
These methods will only handle the allocation and initialization of the NSTimer
. You will need to manually add the timer to a runloop.
initWithFireDate:interval:target:selector:userInfo:repeats:
This method needs to be called on an NSTimer
that you have already called alloc on. You will still need to manually add the timer to a runloop. There is no reason to use this method for new code, unless you just like to call alloc
yourself.
You should’ve noticed that there are two versions of most of those methods. One version takes an NSInvocation
object, the other takes a target,
a selector and something called userInfo
. If you take a look at the NSInvocation
class, you can see that it’s a wrapper around a target and
a selector, along with the ability to pass arguments as well. Conceptually, you can think of the second version with the target, selector and userInfo
as a wrapper around the first that will wrap your target and selector into a NSInvocation
with the userInfo
as the only parameter. There are almost
certainly differences in the actual implementation, but it should behave as if it was implementated that way. You can choose either of them, but
I’ve found that the using the target, selector and userInfo
version is typically the easier way to go.
You might be wondering why you wouldn’t just always have it schedule the timer on the current run loop? Well it’s possible that you are configuring
a timer that needs to be attached to a run loop on a different thread. If you were running on a different thread than the main thread and your timer needed to update UI state, you would want to make sure that the timer ran on the main thread and not the current thread. If you do need to handle this, you will need to call addTimer:forMode:
on the desired NSRunLoop
to schedule it.
One thing you didn’t see as an option above is a blocks based method for timers. Apple hasn’t (as of Nov 2015) added blocks APIs to NSTimer
. Which means that by default, NSTimers
will be a little tougher to use than they probably should be. If you really want a blocks API for NSTimer
, it’s fairly trivial to implement one using a category. There are also many implementations on the web, so go check those out if your’re interested.
So now you’ve created an NSTimer
– now what? If you created a one-shot timer (i.e, repeats
is NO
) and there is no need for you to cancel the timer later, you are pretty much done once it’s been scheduled on a run loop. You can even ignore the NSTimer
object that was returned to you (if you are using ARC, that is. You are using ARC, right?). If you have a repeating timer or you need the ability to cancel the timer, then you need to keep a reference to the NSTimer
. But how?
This is one of the trickier bits about NSTimer
. The normal Cocoa way would be to put it into a strong reference variable somewhere and then reference it as needed. You might even set it to nil
in your dealloc
. And this is almost certainly always wrong for NSTimer
.
If you look at the documentation, you’ll find out that NSTimer
holds a strong reference to the target (either directly or through the NSInvocation
object). If that target happens to be the class that is holding a strong reference to the NSObject
, bad things will happen. You’ve just created a circular reference between the two objects and they will never be released.
“But I need the timer to stick around. I can’t just put it into a weak reference variable and expect it to stay around!” Actually, you can. That’s because once it’s been scheduled on a runloop, the runloop now has a strong reference to it. Which means you can use a weak reference and you’ve now broken the circular reference cycle.
So if you have a weak reference to the NSTimer
, how would you actually go about canceling it? This is where the invalidate
methods comes in. You can call invalidate on a NSTimer
object, and that will cause the runloop that it is scheduled on to release it. Since you have a weak reference, your reference will automatically be set to nil
and nothing will leak.
Also if you are not automatically adding your NSTimer
to your current run loop, you will need a temporary strong reference to hold your NSTimer
object while you configure it. Hopefully this example will make this a bit clearer:
// in your @interface
@property(nonatomic, weak, readwrite) NSTimer *myTimer;
// in your method where you create the timer
NSTimer *tmpTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:tmpTimer forMode:NSDefaultRunLoopMode];
self.myTimer = tmpTimer;
If you didn’t have a strong reference to your created timer before it was added to a run loop, you would quickly find out that you didn’t have a timer object anymore. Without it being added to a runloop, nobody is holding a reference. So you need to make sure you have a strong reference at least until you’ve added it.
One more note about NSTimer
. Under no circumstances should you ever attempt to invalidate your timer in the dealloc
method of the target object. And the reason should be obvious now. The NSTimer
has a strong reference to your target object, which means that dealloc will never be called as long as the NSTimer
is scheduled. You should always invalidate in some other method that can be called before the object is expected to go away. For example, if you have a timer in a UIViewController
subclass, you can use viewWillAppear
and viewWillDisappear
as the place to create and invalidate your timers.
Hopefully you’ve learned a little more about NSTimer
now. It’s a simple class with some subtle corner cases. But with just a little bit of understanding of how it works, you can keep yourself out of those corners.
Nov 20, 2015