NSTimer學習筆記

NSTimer是iOS最經常使用的定時器工具之一,在使用的時候經常會遇到各類各樣的問題,最多見的是內存泄漏,一般咱們使用NSTimer的通常流程是這樣的bash

  1. 在ViewController初始化或加載的地方建立NSTimer,而且經過屬性持有(爲了關閉)多線程

  2. 在ViewController的dealloc方法關閉定時器(invalidate),而且把NSTimer置爲nilapp

上面作法可能會形成內存泄漏,invalidate方法一般不能放在NStimer.target.dealloc裏面,由於NSTimer會對target強引用,而若是target對NSTimer強引用就會形成循環引用框架

1. 構造函數

NSTimer只有被添加的Runloop才能生效,NSTimer有下面兩種類型的構造函數函數

  • initWithFireDate工具

  • timerWithTimeIntervaloop

  • scheduledTimerWithTimeIntervalatom

scheduledTimerWithTimeInterval除了構造timer,還會把timer添加到當前線程的runloop,因此咱們一般使用scheduledTimerWithTimeInterval構造NSTimer而不是timerWithTimeInterval線程

  1. 沒有添加到runloop的timer,調用fire的時候會直接觸發,而且只觸發一次(若是repeat:YES)code

- (void)viewDidLoad {
    [super viewDidLoad];

    [self timer1];
    //[self timer2];
    //[self timer3];
    //[self timer4];
}

- (void)timer1 {
    self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
    // 不會觸發
}

- (void)timer2 {
    self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
    // 正常觸發
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (IBAction)invalidate:(id)sender {
    [self.timer invalidate];
    self.timer = nil;
}

- (void)timerTest:(NSObject *)obj {
    NSLog(@"time fire");
}
  1. 若是使用timerWithTimeIntervalinitWithFireDate構造,須要手動添加到runloop上,使用scheduledTimerWithTimeInterval則不須要

- (void)timer3 {
    self.timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:3] interval:3 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
    // 須要添加到runloop才能觸發
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)timer4 {
    // 自動添加到runloop
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
}

2. NSTimer的觸發

  • NSTimer在添加到runloop時,timer開始計時,即便runloop沒有開啓(run),在構造NSTimer的時候,若是不是立刻開始計時,能夠先使用timerWithTimeInterval再手動加入runloop上

  • 調用fire的時候,當即觸發timer的方法,該方法觸發不影響計時器本來的計時,只是新增一次觸發

  • 當NSTimer進入後臺的時,NSTimer計時暫停,進入前臺繼續

3. NSTimer和Runloop

上面構造函數咱們能夠看到,當咱們把timer添加到runloop的時候會指定NSRunLoopMode(scheduledTimerWithTimeInterval默認使用NSDefaultRunLoopMode),iOS支持的有下面兩種模式

  • NSDefaultRunLoopMode:默認的運行模式,用於大部分操做,除了NSConnection對象事件。

  • NSRunLoopCommonModes:是一個模式集合,當綁定一個事件源到這個模式集合的時候就至關於綁定到了集合內的每個模式。

下面三種是內部框架支持(AppKit)

  • NSConnectionReplyMode:用來監控NSConnection對象的回覆的,不多可以用到。

  • NSModalPanelRunLoopMode:用於標明和Mode Panel相關的事件。

  • NSEventTrackingRunLoopMode:用於跟蹤觸摸事件觸發的模式(例如UIScrollView上下滾動)。

當timer添加到主線程的runloop時,某些UI事件(如:UIScrollView的拖動操做)會將runloop切換到NSEventTrackingRunLoopMode模式下,在這個模式下,NSDefaultRunLoopMode模式註冊的事件是不會被執行的,也就是經過scheduledTimerWithTimeInterval方法添加到runloop的NSTimer這時候是不會被執行的

爲了讓NSTimer不被UI事件干擾,咱們須要將註冊到runloop的timer的mode設爲NSRunLoopCommonModes,這個模式等效於NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的結合

// 主線程
self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

4. 循環引用

循環引用是最常常遇到的問題之一

  • NSTimer在構造函數會對target強引用,在調用invalidate時,會移除去target的強引用

    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:@"ghi" repeats:YES];
    
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
    
    [timer invalidate];
    
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
    輸出以下
    2017-05-09 10:41:45.071 NSTimerTest[6861:914021] Retain count is 6
    2017-05-09 10:41:46.056 NSTimerTest[6861:914021] Retain count is 7
    2017-05-09 10:41:47.848 NSTimerTest[6861:914021] Retain count is 6
  • NSTimer被加到Runloop的時候,會被runloop強引用持有,在調用invalidate的時候,會從runloop刪除

    NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:@"ghi" repeats:YES];
    
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)timer));
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)timer));
    
    [timer invalidate];
    
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)timer));
    輸出以下
    2017-05-09 09:37:30.573 NSTimerTest[6505:883666] Retain count is 1
    2017-05-09 09:37:33.177 NSTimerTest[6505:883666] Retain count is 2
    2017-05-09 09:38:19.111 NSTimerTest[6505:883666] Retain count is 1
  • 當定時器是不重複的(repeat=NO),在執行完觸發函數後,會自動調用invalidate解除runloop的註冊和接觸對target的強引用

因爲NSTimer被加到runloop的時候會被runloop強引用,故若是使用scheduledTimerWithTimeInterval構造函數時,咱們能夠在viewcontroller使用weak引用NSTimer

@property (nonatomic, weak) NSTimer *timer;
...

- (void)viewDidLoad {
    [super viewDidLoad];

    // 因爲timer會被當前線程的runloop持有,故可使用weak引用,而當調用invalidate時,self.timer會被自動置爲nil
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];

    // 或者
    NSTimer *timer2 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];
    self.timer = timer;

}

因此一般咱們不能在dealloc方法讓[timer invalidate], 由於timer在invalidate以前,會引用self(一般是ViewController),致使self沒法釋放,能夠在viewDidDisappear或顯式調用timer的invalidate方法

invalidate是惟一讓timer從runloop刪除的方法,也是惟一去除對target強引用的方法

5. 多線程

若是咱們不在主線程使用Timer的時候,即便咱們把timer添加到runloop,也不能被觸發,由於主線程的runloop默認是開啓的,而其餘線程的runloop默認沒有實現runloop,而且在後臺線程使用NSTimer不能經過fire啓動定時器,只能經過runloop不斷的運行下去

- (void)viewDidLoad {
    [super viewDidLoad];

    // 使用新線程
    [NSThread detachNewThreadSelector:@selector(startNewThread) toTarget:self withObject:nil];
}

- (void)startNewThread {
    self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];

    // 添加到runloop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];

    // 非主線程須要手動運行runloop,run方法會阻塞,直到沒有輸入源的時候返回(例如:timer從runloop中移除,invalidate)
    [runLoop run]
}

6. NSTimer準確性

一般咱們使用NSTimer的時候都是在主線程使用的,主線程負責不少複雜的操做,例如UI處理,UI時間響應,而且iOS上的主線程是優先響應UI事件的,而NSTimer的優先級較低,有時候NSTimer的觸發並不許確,例如當咱們在滑動UIScrollView的時候,NSTimer就會延遲觸發,主線優先響應UI的操做,只有UIScrollView中止了才觸發NSTimer的事件
解決方案
NSTimer加入到runloop默認的Mode爲NSDefaultRunLoopMode, 咱們須要手動設置Mode爲NSRunLoopCommonModes
這時候,NSTimer即便在UI持續操做過程當中也能獲得觸發,固然,會下降流暢度

NSTimer觸發是不精確的,若是因爲某些緣由錯過了觸發時間,例如執行了一個長時間的任務,那麼NSTimer不會延後執行,而是會等下一次觸發,至關於等公交錯過了,只能等下一趟車,tolerance屬性能夠設置偏差範圍

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
// 偏差範圍1s內
timer.tolerance = 1;

若是對精度有要求,可使用GCD的定時器

7 NSTimer暫停/繼續

NSTimer不支持暫停和繼續,若是須要可使用GCD的定時器

8. 後臺運行

NSTimer不支持後臺運行(真機),可是模擬器上App進入後臺的時候,NSTimer還會持續觸發

若是須要後臺運行能夠經過下面兩種方式支持

  1. 讓App支持後臺運行(運行音頻)(在後臺能夠觸發)

  2. 記錄離開和進入App的時間,手動控制計時器(在後臺不能觸發)

第一種控制起來比較麻煩,一般建議手動控制,不在後臺觸發計時

9. performSelector

NSObject對象有一個performSelector能夠用於延遲執行一個方法,其實該方法內部是啓用一個Timer並添加到當前線程的runloop,原理與NSTimer同樣,因此在非主線程使用的時候,須要保證線程的runloop是運行的,不然不會獲得執行

以下

- (void)viewDidLoad {
    [super viewDidLoad];

    [NSThread detachNewThreadSelector:@selector(startNewThread) toTarget:self withObject:nil];
}

- (void)startNewThread {
    // test方法不會觸發,由於runloop默認不開啓
    [self performSelector:@selector(test) withObject:nil afterDelay:1];
}

- (void)test {
    NSLog(@"test trigger");
}

10. 總結

總的來講使用NSTimer有兩點須要注意

  1. NSTimer只有被註冊到runloop才能起做用,fire不是開啓定時器的方法,只是觸發一次定時器的方法

  2. NSTimer會強引用target

  3. invalidate取消runloop的註冊和target的強引用,若是是非重複的定時器,則在觸發時會自動調用invalidate

一般咱們本身封裝GCD定時器使用起來更爲方便,不會有這些問題

相關文章
相關標籤/搜索