6

Here's my issue:

I have a model class that has an NSTimer in it that I want the Timer to run for the entire lifespan of the model object. Initiliazation is easy: I just have the following line of code in the init method:

self.maintainConnectionTimer = 
             [NSTimer scheduledTimerWithTimeInterval:1 
                                              target:self 
                                            selector:@selector(maintainConnection) 
                                            userInfo:nil 
                                             repeats:YES];

However, my issue is, how do I invalidate this timer when the model is released from memory? Now, this would usually be easy, however, as far as I know, when you schedule an NSTimer the OS maintains a strong pointer to the Timer object.

How should I deal with this? Is there a method that gets called right before the model is released from memory?

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Nosrettap
  • 10,940
  • 23
  • 85
  • 140
  • 1
    I've never really used this before...When I was learning Objective-C I was always told that `dealloc` is seldom used anymore. Will my properties still be valid in the `dealloc` method? – Nosrettap Jan 23 '13 at 22:05
  • How about dealloc? Yes, they will be. I'm typing it up as an answer. – s.bandara Jan 23 '13 at 22:05
  • Cool! If you post this as an answer I'll accept it – Nosrettap Jan 23 '13 at 22:11
  • Check the comment's by others. You will have to strip down with a different strategy. – s.bandara Jan 23 '13 at 22:24
  • Whenever you are done with the object, you might have to manually invalidate the timer. As long as the timer is running, it will not be released. – iDev Jan 23 '13 at 22:29

2 Answers2

22

The [NSTimer scheduledTimerWithTimeInterval:...] retains the target, so if the target is self, then your instance of the model class will never be deallocated.

As a workaround, one can use a separate object (called TimerTarget in the following example). TimerTarget has a weak reference to ModelClass, to avoid a retain cycle.

This "helper class" looks like this. Its only purpose is to forward the timer event to the "real target".

@interface TimerTarget : NSObject
@property(weak, nonatomic) id realTarget;
@end

@implementation TimerTarget

- (void)timerFired:(NSTimer*)theTimer
{
    [self.realTarget performSelector:@selector(timerFired:) withObject:theTimer];
}

@end

Now, in your model class, you can create a timer and invalidate it in dealloc:

@interface ModelClass ()
@property(strong, nonatomic) NSTimer *timer;
@end

@implementation ModelClass

- (id)init
{
    self = [super init];
    if (self) {
        TimerTarget *timerTarget = [[TimerTarget alloc] init];
        timerTarget.realTarget = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                                 target:timerTarget
                                               selector:@selector(timerFired:)
                                               userInfo:nil repeats:YES];
    }
    return self;
}

- (void)dealloc
{
    [self.timer invalidate]; // This releases the TimerTarget as well!
    NSLog(@"ModelClass dealloc");
}

- (void)timerFired:(NSTimer*)theTimer
{
    NSLog(@"Timer fired");
}

@end

So we have

modelInstance ===> timer ===> timerTarget ---> modelInstance
(===> : strong reference, ---> : weak reference)

Note that there is no (strong) reference from the timer to the instance of the model class anymore.

I have tested this with the following code, which creates an instance of ModelClass and releases it after 5 seconds:

__block ModelClass *modelInstance = [[ModelClass alloc] init];
int64_t delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    modelInstance = nil;
});

Output:

2013-01-23 23:54:11.483 timertest[16576:c07] Timer fired
2013-01-23 23:54:12.483 timertest[16576:c07] Timer fired
2013-01-23 23:54:13.483 timertest[16576:c07] Timer fired
2013-01-23 23:54:14.483 timertest[16576:c07] Timer fired
2013-01-23 23:54:15.483 timertest[16576:c07] Timer fired
2013-01-23 23:54:15.484 timertest[16576:c07] ModelClass dealloc
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 2
    +1, Since there are no other straightforward ways to do this, I think this would be the better option in this case. – iDev Jan 23 '13 at 23:13
  • 3
    One could probably encapsulate this in a custom `AutoreleasingTimer` class. – Martin R Jan 23 '13 at 23:19
  • According to the Apple docs, we 'should not attempt to subclass NSTimer'. Maybe such an AutoreleasingTimer class could have factory methods that return NSTimer objects with their targets set to an internal AutoreleasingTimerTarget class? – David Schwartz Jul 22 '15 at 22:28
  • 1
    @DavidSchwartz: You are right, and subclassing NSTimer is not what I meant. – Martin R Jul 23 '15 at 06:56
  • 1
    Ah right - after rereading your comment I realize that you never suggested subclassing NSTimer. My apologies. – David Schwartz Jul 23 '15 at 07:06
  • I am wondering this is really the only way. Why doesn't Apple provide something else or are there better approaches when dealing with timing next to `NSTimer`? – Alex Cio Aug 13 '16 at 16:38
0

Based on @Martin R idea, I create custom class which easier to use, and add some checking to avoid crash.

@interface EATimerTarget : NSObject

// Initialize with block to avoid missing call back
- (instancetype)initWithRealTarget:(id)realTarget timerBlock:(void(^)(NSTimer *))block;

// For NSTimer @selector() parameter
- (void)timerFired:(NSTimer *)timer;

@end

@interface EATimerTarget ()
@property (weak, nonatomic) id realTarget;
@property (nonatomic, copy) void (^timerBlock)(NSTimer *); // use 'copy' to avoid retain counting
@end

@implementation EATimerTarget

- (instancetype)initWithRealTarget:(id)realTarget timerBlock:(void (^)(NSTimer *))block {
    self = [super init];
    if (self) {
        self.realTarget = realTarget;
        self.timerBlock = block;
    }
    return self;
}

- (void)timerFired:(NSTimer *)timer {
    // Avoid memory leak, timer still run while our real target is dealloc
    if (self.realTarget) {
        self.timerBlock(timer);
    }
    else {
        [timer invalidate];
    }
}

@end

Here is my sample class

@interface MyClass
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation MyClass

- (id)init
{
    self = [super init];
    if (self) {
         // Using __weak for avoiding retain cycles
         __weak typeof(self) wSelf = self;
    EATimerTarget *timerTarget = [[EATimerTarget alloc] initWithRealTarget:self timerBlock: ^(NSTimer *timer) {
        [wSelf onTimerTick:timer];
    }];
         self.timer = [NSTimer timerWithTimeInterval:1 target:timerTarget selector:@selector(timerFired:) userInfo:nil repeats:YES];
         [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    }
    return self;
}

- (void)dealloc
{
    [self.timer invalidate]; // This releases the EATimerTarget as well! 
    NSLog(@"### MyClass dealloc");
}

- (void)onTimerTick:(NSTimer *)timer {
     // DO YOUR STUFF!
     NSLog(@"### TIMER TICK");
}

@end
nahung89
  • 7,745
  • 3
  • 38
  • 40