建立一個定時器(NSTimer)
- (void)viewDidLoad { [super viewDidLoad]; [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(actionTimer:) userInfo:nil repeats:YES];}- (void)actionTimer:(NSTimer *)timer{}
NSTimer預設運行在default mode下,default mode幾乎包括所有輸入源(除NSConnection) NSDefaultRunLoopMode模式。
actionTimer方法會每隔1s中被調用一次。NSTimer使用起來是不是非常簡單。這是NSTimer比較初級的應用。
當主介面被滑動時NSTimer失效了
主介面被滑動是什麼意思呢?就是說主介面有UITableView或者UIScrollView,滑動UITableView或者UIScrollView。這個時候NSTimer失效了。
我們來寫一個demo,在一個有UITableView的UIViewController上啟動定時器,每1s數字加1,並將這個數字顯示在UILabel上面.
- (void)viewDidLoad { [super viewDidLoad]; [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(actionTimer:) userInfo:nil repeats:YES];}- (void)actionTimer:(NSTimer *)timer{ self.number++; self.label.text = [NSString stringWithFormat:@"%d",self.number]; NSLog(@"%d",self.number);}
關於UITableView和UILabel的建立我省去了。詳細的代碼可以點擊這裡下載:iOSStrongDemo,iOSStrongDemo我會不斷更新,大家在github上star一下。
這樣當使用者在拖動UITableView處於UITrackingRunLoopMode時,NSTimer就失效了,不能fire。self.label上的數字也就無法更新。
修改NSTimer的run loop
解決方案就是將其加入到UITrackingRunLoopMode模式或NSRunLoopCommonModes模式中。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
或者
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes:是一個模式集合,當綁定一個事件來源到這個模式集合的時候就相當於綁定到了集合內的每一個模式。
fire
我們先用 NSTimer 來做個簡單的計時器,每隔5秒鐘在控制台輸出 Fire 。比較想當然的做法是這樣的:
@interface DetailViewController ()@property (nonatomic, weak) NSTimer *timer;@end@implementation DetailViewController- (IBAction)fireButtonPressed:(id)sender { _timer = [NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:@selector(timerFire:) userInfo:nil repeats:YES]; [_timer fire];}-(void)timerFire:(id)userinfo { NSLog(@"Fire");}@end
運行之後確實在控制台每隔3秒鐘輸出一次 Fire ,然而當我們從這個介面跳轉到其他介面的時候卻發現:控制台還在源源不斷的輸出著 Fire 。看來 Timer 並沒有停止。
invalidate
既然沒有停止,那我們在 DemoViewController 的 dealloc 裡加上 invalidate 的方法:
-(void)dealloc { [_timer invalidate]; NSLog(@"%@ dealloc", NSStringFromClass([self class]));}
再次運行,還是沒有停止。原因是 Timer 添加到 Runloop 的時候,會被 Runloop 強引用:
Note in particular that run loops maintain strong references to their timers, so you don't have to maintain your own strong reference to a timer after you have added it to a run loop.
然後 Timer 又會有一個對 Target 的強引用(也就是 self ):
Target is the object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
也就是說 NSTimer 強引用了 self ,導致 self 一直不能被釋放掉,所以也就走不到 self 的 dealloc 裡。
既然如此,那我們可以再加個 invalidate 按鈕:
- (IBAction)invalidateButtonPressed:(id)sender { [_timer invalidate];}
嗯這樣就可以了。(在 SOF 上有人說該在 invalidate 之後執行 _timer = nil ,未能理解為什麼,如果你知道原因可以告訴我:)
在 invalidate 方法的文檔裡還有這這樣一段話:
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
NSTimer 在哪個線程建立就要在哪個線程停止,否則會導致資源不能被正確的釋放。看起來各種坑還不少。
dealloc
那麼問題來了:如果我就是想讓這個 NSTimer 一直輸出,直到 DemoViewController 銷毀了才停止,我該如何讓它停止呢?
- NSTimer 被 Runloop 強引用了,如果要釋放就要調用 invalidate 方法。
- 但是我想在 DemoViewController 的 dealloc 裡調用 invalidate 方法,但是 self 被 NSTimer 強引用了。
- 所以我還是要釋放 NSTimer 先,然而不調用 invalidate 方法就不能釋放它。
- 然而你不進入到 dealloc 方法裡我又不能調用 invalidate 方法。
- 嗯…