標籤:ios
NSTimer
A timer provides a way to perform a delayed action or a periodic action. The timer waits until a certain time interval has elapsed and then fires, sending a specified message to a specified object(timer就是一個能在從現在開始的未來的某一個時刻又或者周期性的執行我們指定的方法的對象)。
- NSTimer會retain你添加調用方法的對象
- NSTimer是要加到runloop中才會起作用
- NSTimer會並不是準確的按照你指定的時間觸發的
- NSTimer就算添加到runloop了也不一定會按照你想象中的那樣執行
NSTimer和它調用的函數對象間到底發生了什嗎?
那麼timer會在未來的某個時刻執行一次或者多次我們指定的方法,這也就牽扯出一個問題,如何保證timer在未來的某個時刻觸發指定事件的時候,我們指定的方法是有效呢?
解決方案很簡單,只要將指定給timer的方法的接收者retain一份就搞定了,實際上系統也是這樣做的。不管是重複性的timer還是一次性的timer都會對它的方法的接收者進行retain,這兩種timer的區別在於“一次性的timer在完成調用以後會自動將自己invalidate,而重複的timer則將永生,直到你顯示的invalidate它為止”。
一方面,NSTimer經常會被作為某個類的成員變數,而NSTimer初始化時要指定self為target,容易造成循環參考。 另一方面,若 timer一直處於validate的狀態,則其引用計數將始終大於0。
#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>@interface TsetClass : NSObject- (void)clearTimer;@end// TsetClass.m// TestRuntime//// Created by 58 on 15/6/1.// Copyright (c) 2015年 58. All rights reserved.//#import "TsetClass.h"@interface TsetClass (){ NSTimer *_myTimer;}@end@implementation TsetClass - (id)init { if (self = [super init]) { _myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES]; } return self; }- (void)handleTimer:(id)sender{ NSLog(@"%@ say: testTimer!", [self class]);} - (void)clearTimer {[_myTimer invalidate]; _myTimer = nil; }- (void)dealloc { [self clearTimer]; NSLog(@"[Friend class] is dealloced"); }@end
在類外部初始化一個TsetClass對象,並延遲5秒後將friend釋放(外部運行在非arc環境下)
TsetClass *f = [[TsetClass alloc] init]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [f release]; });
我們所期待的結果是,初始化5秒後,f對象被release,f的dealloc方法被調用,在dealloc裡面timer失效,對象被析構。但結果卻是如此:
2015-06-02 14:04:08.229 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:09.231 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:10.227 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:11.228 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:12.231 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:13.227 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:14.229 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:15.231 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:16.229 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:17.231 TestRuntime[8883:135129] TsetClass say: testTimer!2015-06-02 14:04:18.230 Te...
這是為什麼呢?主要是因為從timer的角度,timer認為調用方(Friend對象)被析構時會進入dealloc,在dealloc可以順便將timer的計時停掉並且釋放記憶體;但是從Friend的角度,他認為timer不停止計時不析構,那我永遠沒機會進入dealloc。循環參考,互相等待,子子孫孫無窮盡也。問題的癥結在於-(void)cleanTimer函數的調用時機不對,顯然不能想當然地放在調用者的dealloc中。一個比較好的解決方案是開放這個函數,讓Friend的調用者顯式地調用來清理現場。如下:
TsetClass *f = [[TsetClass alloc] init];dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [f clearTimer]; [f release];});
到此timer的循環參考就說完了!!!
綜上: timer都會對它的target進行retain,我們需要小心對待這個target的生命週期問題,尤其是重複性的timer。(NSTimer初始化後,self的retainCount加1。 那麼,我們需要在釋放這個類之前,執行[timer invalidate];否則,不會執行該類的dealloc方法。)
那麼Timer 的使用你到底會了嗎
NSTimer不是一個即時系統,因此不管是一次性的還是周期性的timer的實際觸發事件的時間可能都會跟我們預想的會有出入。差距的大小跟當前我們程式的執行情況有關係,比如可能程式是多線程的,而你的timer只是添加在某一個線程的runloop的某一種指定的runloopmode中,由於多線程通常都是分時執行的,而且每次執行的mode也可能隨著實際情況發生變化。假設你添加了一個timer指定2秒後觸發某一個事件,但是簽好那個時候當前線程在執行一個連續運算(例如大資料區塊的處理等),這個時候timer就會延遲到該連續運算執行完以後才會執行。重複性的timer遇到這種情況,如果延遲超過了一個周期,則會和後面的觸發進行合并,即在一個周期內只會觸發一次。但是不管該timer的觸發時間延遲的有多離譜,他後面的timer的觸發時間總是倍數於第一次添加timer的間隙。
當線程閒置時候timer的訊息觸發還是比較準確的
NSTimer為什麼要添加到RunLoop中才會有作用
前面的例子中我們使用的是一種便利方法,它其實是做了兩件事:首先建立一個timer,然後將該timer添加到當前runloop的default mode中。也就是這個便利方法給我們造成了只要建立了timer就可以生效的錯覺,我們當然可以自己建立timer,然後手動的把它添加到指定runloop的指定mode中去。
NSTimer其實也是一種資源,如果看過多線程變成指引文檔的話,我們會發現所有的source如果要起作用,就得加到runloop中去。同理timer這種資源要想起作用,那肯定也需要加到runloop中才會又效嘍。如果一個runloop裡面不包含任何資源的話,運行該runloop時會立馬退出。你可能會說那我們APP的主線程的runloop我們沒有往其中添加任何資源,為什麼它還好好的運行。我們不添加,不代表架構沒有添加,如果有興趣的話你可以列印一下main thread的runloop,你會發現有很多資源。
NSTimer加到了RunLoop中但遲遲的不觸發事件
- runloop是否運行
每一個線程都有它自己的runloop,程式的主線程會自動的使runloop生效,但對於我們自己建立的線程,它的runloop是不會自己運行起來,當我們需要使用它的runloop時,就得自己啟動。
那麼如果我們把一個timer添加到了非主線的runloop中,它不會按照預期按時觸發
- mode是否正確
我們前面自己動手添加runloop的時候,可以看到有一個參數runloopMode,這個參數是幹嘛的呢?
前面提到了要想timer生效,我們就得把它添加到指定runloop的指定mode中去,通常是主線程的defalut mode。但有時我們這樣做了,卻仍然發現timer還是沒有觸發事件。這是為什麼呢?
這是因為timer添加的時候,我們需要指定一個mode,因為同一線程的runloop在啟動並執行時候,任意時刻只能處於一種mode。所以只能當程式處於這種mode的時候,timer才能得到觸發事件的機會。
舉個不恰當的例子,我們說兄弟幾個分別代表runloop的mode,timer代表他們自己的才水桶,然後一群人去排隊打水,只有一個水龍頭,那麼同一時刻,肯定只能有一個人處於接水的狀態。也就是說你雖然給了老二一個桶,但是還沒輪到它,那麼你就得等,只有輪到他的時候你的水桶才能碰上用場
綜上: 要讓timer生效,必須保證該線程的runloop已啟動,而且其啟動並執行runloopmode也要匹配。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
iOS容易造成循環參考的三種情境NSTimer以及對應的使用方法(一)