开发者

NSTimer as a self-targeting ivar

开发者 https://www.devze.com 2023-02-04 08:56 出处:网络
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

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, t开发者_StackOverflow中文版his 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.


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.


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)..


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];
    }];


For the sake of completness, I will post my own solution of a timer which doesn't retain its target. The solution with a proxy is probably the simplest but the one I have is not bad either.

First we have to understand that NSTimer is toll free bridged to a class in Core Foundation: CFRunLoopTimerRef. And Core Foundation classes in general have much bigger possibilites.

@implementation Target

- (void)timerFired:(NSTimer*)timer {
   ...
}

static void timerCallback(CFRunLoopTimerRef timer, void *info) {
    Target* target = (Target*) info;

    [target timerFired:(NSTimer*) timer];
}

- (void)startTimer {
    //timer not retaining its context

    CFRunLoopTimerContext timerContext;
    timerContext.version = 0;
    timerContext.info = self;
    timerContext.retain = NULL;
    timerContext.release = NULL;
    timerContext.copyDescription = NULL;

    //this is a repeating timer
    CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
                                                   CFAbsoluteTimeGetCurrent() + FIRE_INTERVAL,
                                                   FIRE_INTERVAL,
                                                   0,
                                                   0,
                                                   timerCallback,
                                                   &timerContext);

    //schedule the timer
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);

    self.timer = (NSTimer*) timer;

    CFRelease(timer);
}
@end

MRC is used here, with ARC it needs some bridging casts.


In a similar situation (on iOS, for a UIView subclass) I piggybacked on removeFromSuperview, and as a double check I test the .superview property every time the timer fires.

Are you sure you don't have any property or call on the class which effectively means that it's about to die?


For repeating timer, you do not need the ivar, just:

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick) userInfo:nil repeats:YES];    

The timer will repeats till end of the program.

For a non-repeating timer, you may check out: performSelector.


You could use an custom setter to handle this. The setter should release self every time the timers target is self and release everytime it was but is no longer.

This sample of code is working and ensures that the retainCount is correct at all times.

Example of custom setter

- (void)setMyTimer:(NSTimer *)aNewTimer { // timer retains target/object (self)
    if(aNewTimer != myTimer) {   


    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

        // since myTimer was retaining its target (self) and aNewTimer is not retaining self (aNewTimer is nil) and will not retain self you – should retain self
    if(aNewTimer == nil && myTimer != nil) {
        [self retain];
    }
        // since old timer was not retaining self since myTimer is nil and aNewTimer is retaining self – you should release
    else if(aNewTimer != nil && myTimer == nil) {
        [self release]; 
    }

        [myTimer invalidate];
        [myTimer release];
        myTimer = [aNewTimer retain];

    [pool drain], pool = nil;

    }
}

Remember everytime the timer fires you should set it to nil.

- (void)timerDidFire:(NSTimer *)aTimer { 
    if(aTimer == self.myTimer) {
        self.myTimer = nil;
    }
}

And in dealloc remember to set self.myTimer = nil


You could also use an proxy-object which does not retain self but assigns.. would be clean.

0

精彩评论

暂无评论...
验证码 换一张
取 消