How to know when to invalidate an `NSTimer`

前端 未结 2 1840
无人共我
无人共我 2020-12-16 05:22

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

相关标签:
2条回答
  • 2020-12-16 05:42

    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
    
    0 讨论(0)
  • 2020-12-16 06:04

    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
    
    0 讨论(0)
提交回复
热议问题