Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSTimer as a self-targeting ivar

I have come across an awkward situation where I would like to have a class with an NSTimer instance variable that repeatedly calls a method of the class as long as the class is alive. For illustration purposes, it might look like this:

// .h
@interface MyClock : NSObject {
    NSTimer* _myTimer;
}
- (void)timerTick;
@end

-

// .m
@implementation MyClock

- (id)init {
    self = [super init];
    if (self) {
        _myTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTick) userInfo:nil repeats:NO] retain];
    }
    return self;
}

- (void)dealloc {
    [_myTimer invalidate];
    [_myTImer release];
    [super dealloc];
}

- (void)timerTick {
    // Do something fantastic.
}

@end

That's what I want. I don't want to to have to expose an interface on my class to start and stop the internal timer, I just want it to run while the class exists. Seems simple enough.

But the problem is that NSTimer retains its target. That means that as long as that timer is active, it is keeping the class from being dealloc'd by normal memory management methods because the timer has retained it. Manually adjusting the retain count is out of the question. This behavior of NSTimer seems like it would make it difficult to ever have a repeating timer as an ivar, because I can't think of a time when an ivar should retain its owning class.

This leaves me with the unpleasant duty of coming up with some method of providing an interface on MyClock that allows users of the class to control when the timer is started and stopped. Besides adding unneeded complexity, this is annoying because having one owner of an instance of the class invalidate the timer could step on the toes of another owner who is counting on it to keep running. I could implement my own pseudo-retain-count-system for keeping the timer running but, ...seriously? This is way to much work for such a simple concept.

Any solution I can think of feels hacky. I ended up writing a wrapper for NSTimer that behaves exactly like a normal NSTimer, but doesn't retain its target. I don't like it, and I would appreciate any insight.

like image 950
Matt Wilding Avatar asked Jan 14 '11 03:01

Matt Wilding


3 Answers

The retain loops caused by timers are a pain in the neck. The least fragile approach I've used is to not retain the timer, but always set the reference to nil when invalidating it.

@interface Foo : NSObject
{
    __weak NSTimer *_timer;
}
@end

@implementation Foo
- (void) foo
{
    _timer = [NSTimer ....self....];
}

- (void) reset
{
    [_timer invalidate], _timer = nil;
}

- (void) dealloc
{
    // since the timer is retaining self, no point in invalidating here because
    // that just can't happen
    [super dealloc];
}
@end

The bottom line, though, is that you'll have to have something call -reset for self to be released. A solution is to stick a proxy between self and the timer. The proxy can have a weak reference to self and simply passes the invocation of the timer firing along to self. Then, when self is deallocated (since the timer doesn't retain self and neither does the proxy), you can call invalidate in dealloc.

Or, if targeting Mac OS X, turn on GC and ignore this nonsense entirely.

like image 99
bbum Avatar answered Nov 19 '22 18:11

bbum


You might want to take a look at NoodleSoft's NSTimer category that allows you to have the timer run a block instead of a selector, thereby avoiding retention of your class as target (and also eliminating the need for a pesky unrelated selector in your code). Check it out here (there's also a bunch of other code you might find useful in there, so take a look around and see what you can find)..

like image 35
Itai Ferber Avatar answered Nov 19 '22 16:11

Itai Ferber


I found a nice solution here:

THInWeakTimer

https://github.com/th-in-gs/THIn

Might be worth considering.

Use it like this:

Have an ivar:

THInWeakTimer *_keepaliveTimer;

In your init method:

    __weak FunderwearConnection* wself = self;
    _keepaliveTimer = [[THInWeakTimer alloc] initWithDelay:kIntervalPeriod do:^{
        [wself doSomething];
    }];
like image 2
Chris Avatar answered Nov 19 '22 18:11

Chris