意外?NSTimerの正しい使い方

iOSでタイマーを使う時、強い参照にしてる方って結構多いんじゃないでしょうか?私もそうだったんですが、先日タイマー関連を見直していて弱参照が使える事に気付きました。

自動的にcurrentRunLoop(for DefaultRunLoopMode)に追加されるscheduledTimerは、すでに強参照で保持されているので、弱参照にしておけばinvalidateで取り除かれた時に即解放となり、nil代入が不要になります。

@property (nonatomic, weak) NSTimer *timer;

- (void)startTimer {
    if (self.timer == nil) {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:60 target:self selector:@selector(update) userInfo:nil repeats:YES];
    }
}

- (void)stopTimer {
    if (self.timer != nil) {
        [self.timer invalidate];
    }
}

Appleのサンプルでもweakを使っていましたが、assignを指定している方もおられますね。

Swift 3.0なら更に短く書けます。

private weak var timer: Timer?

func startTimer() {
    if timer == nil {
        timer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(update), userInfo: nil, repeats: true)
    }
}

func stopTimer() {
    timer?.invalidate()
}

余談

NSTimerに似ているCADisplayLinkはどうなのか検証したところ、実機とシミュレータで異なった結果になりました。

@property (nonatomic, weak) CADisplayLink *link;

self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];

// 実機 iOS 10.0
// self.link is retained

// シミュレータ Xcode 8.3.3
// self.link is null

CADisplayLinkはaddToRunLoopしてから使うでしょうから、strongにしておいて自力で解放する方が良さそうです。

@property (nonatomic) CADisplayLink *link;

- (void)startDisplayLink {
    if (self.link == nil) {
        self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];
        [self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    }
}

- (void)stopDisplayLink {
    if (self.link != nil) {
        [self.link invalidate];
        self.link = nil;
    }
}