objective-cautomatic-ref-countingnstimerretain-cycle

Weak Reference to NSTimer Target To Prevent Retain Cycle


I'm using an NSTimer like this:

timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];

Of course, NSTimer retains the target which creates a retain cycle. Furthermore, self isn't a UIViewController so I don't have anything like viewDidUnload where I can invalidate the timer to break the cycle. So I'm wondering if I could use a weak reference instead:

__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

I've heard that the timer must be invalidated (i guess to release it from the run loop). But we could do that in our dealloc, right?

- (void) dealloc {
    [timer invalidate];
}

Is this a viable option? I've seen a lot of ways that people deal with this issue, but I haven't seen this.


Solution

  • The proposed code:

    __weak id weakSelf = self;
    timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
    

    has the effect that (i) a weak reference is made to self; (ii) that weak reference is read in order to provide a pointer to NSTimer. It won't have the effect of creating an NSTimer with a weak reference. The only difference between that code and using a __strong reference is that if self is deallocated in between the two lines given then you'll pass nil to the timer.

    The best thing you can do is create a proxy object. Something like:

    [...]
    @implementation BTWeakTimerTarget
    {
        __weak target;
        SEL selector;
    }
    
    [...]
    
    - (void)timerDidFire:(NSTimer *)timer
    {
        if(target)
        {
            [target performSelector:selector withObject:timer];
        }
        else
        {
            [timer invalidate];
        }
    }
    @end
    

    Then you'd do something like:

    BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
    timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];
    

    Or even add a class method to BTWeakTimerTarget of the form +scheduledTimerWithTimeInterval:target:selector:... to create a neater form of that code. You'll probably want to expose the real NSTimer so that you can invalidate it, otherwise the rules established will be:

    1. the real target isn't retained by the timer;
    2. the timer will fire once after the real target has begun (and probably completed) deallocation, but that firing will be ignored and the timer invalidated then.