6 votes

Comment savoir quand invalider un `NSTimer` ?

Voici mon problème :

J'ai une classe de modèle qui possède un NSTimer que je veux que le timer fonctionne pendant toute la durée de vie de l'objet modèle. L'initialisation est facile : j'ai juste la ligne de code suivante dans le fichier init méthode :

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

Cependant, mon problème est le suivant , comment invalider cette minuterie lorsque le modèle est libéré de la mémoire ? En général, c'est facile, mais, pour autant que je sache, lorsque vous planifiez un NSTimer l'OS maintient un pointeur fort sur l'objet Timer.

Comment dois-je faire face à cette situation ? Existe-t-il une méthode qui est appelée juste avant que le modèle ne soit libéré de la mémoire ?

22voto

Martin R Points 105727

El [NSTimer scheduledTimerWithTimeInterval:...] conserve la cible donc si la cible est self alors votre instance de la classe modèle ne sera jamais désallouée.

Comme solution de contournement, on peut utiliser un objet séparé (appelé TimerTarget dans l'exemple suivant). TimerTarget a un faible référence à ModelClass pour éviter un cycle de rétention.

Cette "classe d'aide" ressemble à ceci. Son seul but est de transmettre l'événement du timer à la "vraie cible".

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

@implementation TimerTarget

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

@end

Maintenant, dans votre classe modèle, vous pouvez créer une minuterie et l'invalider en 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

Nous avons donc

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

Notez qu'il n'y a plus de référence (forte) de la minuterie à l'instance de la classe modèle.

J'ai testé cela avec le code suivant, qui crée une instance de ModelClass et le relâche après 5 secondes :

__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;
});

Sortie :

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

0voto

dementiazz Points 119

En me basant sur l'idée de @Martin R, j'ai créé une classe personnalisée qui est plus facile à utiliser, et j'ai ajouté quelques vérifications pour éviter le 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

Voici mon exemple de classe

@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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X