NSTimer定時器進階——詳細介紹,循環引用分析與解決

引言

定時器:A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object.
翻譯以下:在固定的時間間隔被觸發,而後給指定目標發送消息。總結爲三要素吧:時間間隔、被觸發、發送消息(執行方法)html

按照官方的描述,咱們也確實是這麼用的;可是裏面有不少細節,你是否瞭解呢?app

  • 它會被添加到runloop,不然不會運行,固然添加的runloop不存在也不會運行;
  • 還要指定添加到的runloop的哪一個模式,並且還能夠指定添加到runloop的多個模式,模式不對也是不會運行的
  • runloop會對timer有強引用,timer會對目標對象進行強引用(是否隱約的感受到坑了。。。)
  • timer的執行時間並不許確,系統繁忙的話,還會被跳過去
  • invalidate調用後,timer中止運行後,就必定能從runloop中消除嗎,資源????

呵呵。。。下面會解決這些問題async

定時器的通常用法

控制器中添加定時器,例如:oop

- (void)viewDidLoad {
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

上面的代碼就是咱們使用定時器最經常使用的方式,能夠總結爲2個步驟:建立,添加到runloopui

系統提供了8個建立方法,6個類建立方法,2個實例初始化方法。atom

  • 有三個方法直接將timer添加到了當前runloop default mode,而不須要咱們本身操做,固然這樣的代價是runloop只能是當前runloop,模式是default mode:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
  • 下面五種建立,不會自動添加到runloop,還需調用addTimer:forMode:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

對上面全部方法參數作個說明:spa

  1. ti(interval):定時器觸發間隔時間,單位爲秒,能夠是小數。若是值小於等於0.0的話,系統會默認賦值0.1毫秒
  2. invocation:這種形式用的比較少,大部分都是block和aSelector的形式
  3. yesOrNo(rep):是否重複,若是是YES則重複觸發,直到調用invalidate方法;若是是NO,則只觸發一次就自動調用invalidate方法
  4. aTarget(t):發送消息的目標,timer會強引用aTarget,直到調用invalidate方法
  5. aSelector(s):將要發送給aTarget的消息,若是帶有參數則應:- (void)timerFireMethod:(NSTimer *)timer聲明
  6. userInfo(ui):傳遞的用戶信息。使用的話,首先aSelector須帶有參數的聲明,而後能夠經過[timer userInfo]獲取,也能夠爲nil,那麼[timer userInfo]就爲空
  7. date:觸發的時間,通常狀況下咱們都寫[NSDate date],這樣的話定時器會立馬觸發一次,而且以此時間爲基準。若是沒有此參數的方法,則都是以當前時間爲基準,第一次觸發時間是當前時間加上時間間隔ti
  8. block:timer觸發的時候會執行這個操做,帶有一個參數,無返回值

添加到runloop,參數timer是不能爲空的,不然拋出異常線程

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

另外,系統提供了一個- (void)fire;方法,調用它能夠觸發一次:翻譯

  • 對於重複定時器,它不會影響正常的定時觸發
  • 對於非重複定時器,觸發後就調用了invalidate方法,既使正常的尚未觸發

NSTimer添加到NSRunLoop

如同引言中說的那樣,timer必須添加到runloop纔有效,很明顯要保證兩件事情,一是runloop存在(運行),另外一個纔是添加。確保這兩個前提後,還有runloop模式的問題。代理

一個timer能夠被添加到runloop的多個模式,好比在主線程中runloop通常處於NSDefaultRunLoopMode,而當滑動屏幕的時候,好比UIScrollView或者它的子類UITableView、UICollectionView等滑動時runloop處於UITrackingRunLoopMode模式下,所以若是你想讓timer在滑動的時候也可以觸發,就能夠分別添加到這兩個模式下。或者直接用NSRunLoopCommonModes一個模式集,包含了上面的兩種模式。

可是一個timer只能添加到一個runloop(runloop與線程一一對應關係,也就是說一個timer只能添加到一個線程)。若是你非要添加到多個runloop,則只有一個有效

關於強引用的問題

仍是常用到的代碼

- (void)viewDidLoad {
    // 代碼標記1
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    // 代碼標記2
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 代碼標記3
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

假設代碼中的視圖控制器由UINavigationController管理,且self.timer是strong類型,則強引用能夠表示以下:

timer的強引用

上面有四根強引用線,它們是如何產生的呢,這個也必須搞清楚?

  • L1:這個簡單,nav push 控制器的時候會強引用,即在push的時候產生;
  • L2:是在代碼標記3的位置產生;
  • L3:是在代碼標記1的位置產生,至此L2與L3已經產生了循環引用,雖然timer尚未添加到runloop
  • L4:是在代碼標記2的位置產生

根據上圖就很清晰了,咱們常常說到timer與self會形成循環引用,並非由於runloop引發,而是timer自己會對self有強引用。

invalidate方法

invalidate方法有2個功能:一是將timer從runloop中移除,那麼圖中的L4就消失,二是timer自己也會釋放它持有資源,好比target、userinfo、block(關於block強引用self具體參考這裏:http://www.cnblogs.com/mddblog/p/4754190.html),那麼強引用L3就消失。若是self.timer是weak引用,也就是L2是弱引用,那麼timer的引用計數就爲0了,timer自己也就被釋放了。若是你此時又調用addTimer:forMode:則會拋異常,由於timer爲nil,所以當控制器使用weak方式引用timer時,應注意這點

以後的timer也就永遠無效了,調用它的getter方法isValid返回是NO,即便你再次將它正確的添加到runloop,也不會觸發,由於timer已對target、block釋放了。

timer只有這一個方法能夠完成此操做,因此咱們取消一個timer必需要調用此方法。而在添加到runloop前,可使用它的getter方法isValid來判斷,一個是防止爲nil,另外一個是防止爲無效。

然而就像引言中說的那個聳人聽聞的問題同樣,invalidate方法調用必須在timer添加到的runloop所在的線程,若是不在的話:雖然timer自己會釋放掉它本身持有的資源好比target、userinfo、block,圖中的L3會消失。可是runloop不會釋放timer,即圖中的L4不會消失,假設,self被pop了-->L1無效-->self引用計數爲0,self釋放-->L2也消失。此時就剩runloop、timer、L4,timer也就永遠不會釋放了,形成內存泄露。

下面不得不面對另外一個問題,runloop退出或者自己被釋放不就能夠了嗎???

這才真心是一個頭疼的問題:是的,沒錯,runloop退出甚至自身釋放後,L4消失,timer也就釋放了。。。能夠參考以前那篇關於runloop退出釋放的問題NSRunLoop原理詳解——再也不有盲點:http://www.jianshu.com/p/4263188ed940

這裏補充一點,timer沒有被釋放,那麼它會做爲runloop的輸入源,從而阻止runloop的退出(runloop的退出是會釋放掉timer的)。

只關心runloop的退出就好,至於釋放就別深究了,或者就當它不釋放(個人理解是隨着線程釋放而釋放)

關於強引用再舉個常見例子

重複的添加timer,例以下面的代碼:

// 不管self.timer是strong仍是weak
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(timerHandle) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

每點擊一次屏幕就會添加一次,就會形成重複添加,你的timerHandle方法會被調用屢次,添加幾回就調用幾回。。。

假設點擊了2次屏幕,即建立2了個timer,咱們標記爲t1,t2。咱們分析一下:第二次的時候,self.timer引用t2,雖然不在引用t1可是,runloop還在引用它,因此不會釋放,不用說t2也是不會釋放的。

那麼如何解決呢?setter方法裏面調用invalidate便可:

- (void)setTimer:(NSTimer *)timer {
    [_timer invalidate];
    _timer = timer;
}

其實記住兩條便可

  • timer不用了,必定要調用invalidate
  • 通常是target釋放的同時,纔會知道timer不用了,那麼怎麼捕獲target被釋放了呢?dealloc方法確定是不行的。若是是控制器的話能夠嘗試監聽pop方法的調用(nav的代理),viewDidDisappear方法裏面(但要記着,再次展現的時候重新添加。。。)

不調用invalidate方法,target是不會被釋放的,由於圖中的L4,L3一直存在

timer執行是否準時

不許時!

第一種不許時:有可能跳過去

  1. 線程處理比耗時的事情時會發生
  2. 還有就是timer添加到的runloop模式不是runloop當前運行的模式,這種狀況常常發生。

對於第一種狀況咱們不該該在timer上下功夫,而是應該避免這個耗時的工做。那麼第二種狀況,做爲開發者這也是最應該去關注的地方,要留意,而後視狀況而定是否將timer添加到runloop多個模式

雖然跳過去,可是,接下來的執行不會依據被延遲的時間加上間隔時間,而是根據以前的時間來執行。好比:

定時時間間隔爲2秒,t1秒添加成功,那麼會在t二、t四、t六、t八、t10秒註冊好事件,並在這些時間觸發。假設第3秒時,執行了一個超時操做耗費了5.5秒,則觸發時間是:t二、t8.五、t10,第4和第6秒就被跳過去了,雖然在t8.5秒觸發了一次,可是下一次觸發時間是t10,而不是t10.5。

第二種不許時:不許點

好比上面說的t二、t四、t六、t八、t10,並不會在準確的時間觸發,而是會延遲個很小的時間,緣由也能夠歸結爲2點:

  1. RunLoop爲了節省資源,並不會在很是準確的時間點觸發
  2. 線程有耗時操做,或者其它線程有耗時操做也會影響

以我來說,歷來沒有特別準的時間,

iOS7之後,Timer 有個屬性叫作 Tolerance (時間寬容度,默認是0),標示了當時間點到後,允許有多少最大偏差。

它只會在準確的觸發時間到加上Tolerance時間內觸發,而不會提早觸發(是否是有點像咱們的火車,只會晚點。。。)。另外可重複定時器的觸發時間點不受Tolerance影響,即相似上面說的t8.5觸發後,下一個點不會是t10.5,而是t10 + Tolerance,不讓timer由於Tolerance而產生漂移(忽然想起嵌入式使人頭疼的溫漂)。

其實對於這種不許點,對咱們開發影響並不大(基本是毫秒妙級別如下的延遲),不多會用到很是準點的狀況。

GCD定時器簡單介紹

其實這種咱們平時也常常用(一次性定時):

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

when接受兩種類型參數:dispatch_time相對時間,相對系統的時間,好比上面相對於DISPATCH_TIME_NOW;dispatch_walltime是絕對時間,好比某年月日某時分秒。。。以後由GCD幫咱們計算一個相對時間。下面說下dispatch_time,支持納秒級別

dispatch_time_t when = dispatch_time (DISPATCH_TIME_NOW, 1);// 還沒這麼用過1納秒的延遲

應該很準確了,可是定時時間到後只是將block添加到指定的queue,去執行。這樣的話,執行時間也是不保證的,首先執行線程要等待內核的調度,其次執行線程正好沒有其它事情作。若是還須要建立線程的話,就更浪費時間了。因此這個也是不符合咱們指望的

when也支持DISPATCH_TIME_NOW,可是這樣就沒意義了,不如直接調用dispatch_async。而至於DISPATCH_TIME_FOREVER就更。。。

重複性定時,代碼示例以下:

// 須要強引用
@property (nonatomic, strong)dispatch_source_t gcdTime;

- (void)gcdTimerTest {
    // 這裏須要強引用
    self.gcdTime = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    // 開始時間支持納秒級別
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)2 * NSEC_PER_SEC);
    // 2秒執行一次
    uint64_t dur = (uint64_t)(2.0 * NSEC_PER_SEC);
    // 最後一個參數是容許的偏差,即便設爲零,系統也會有默認的偏差
    dispatch_source_set_timer(self.gcdTime, start, dur, 0);
    // 設置回調
    dispatch_source_set_event_handler(self.gcdTime, ^{
        NSLog(@"---%@---%@",[NSThread currentThread],self);
    });
    dispatch_resume(self.gcdTime);
}

取消定時器:dispatch_cancel(self.gcdTimer);,取消後再次調用dispatch_source_set_timer是沒有用的。self.gcdTimer已不可用

雖然支持納秒級別,可是定時也是不許的,上面的例子使用的是dispatch_get_global_queue隊列,執行線程也是不肯定的。因此在實際開發中這種不多用,好處是它不受runloop mode限制

相關文章
相關標籤/搜索