定時器:A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object.
翻譯以下:在固定的時間間隔被觸發,而後給指定目標發送消息。總結爲三要素吧:時間間隔、被觸發、發送消息(執行方法)html
按照官方的描述,咱們也確實是這麼用的;可是裏面有不少細節,你是否瞭解呢?app
呵呵。。。下面會解決這些問題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
當前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;
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
- (void)timerFireMethod:(NSTimer *)timer
聲明[timer userInfo]
獲取,也能夠爲nil,那麼[timer userInfo]
就爲空添加到runloop,參數timer是不能爲空的,不然拋出異常線程
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
另外,系統提供了一個- (void)fire;
方法,調用它能夠觸發一次:翻譯
如同引言中說的那樣,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類型,則強引用能夠表示以下:
上面有四根強引用線,它們是如何產生的呢,這個也必須搞清楚?
代碼標記3
的位置產生;代碼標記1
的位置產生,至此L2與L3已經產生了循環引用,雖然timer尚未添加到runloop代碼標記2
的位置產生根據上圖就很清晰了,咱們常常說到timer與self會形成循環引用,並非由於runloop引發,而是timer自己會對self有強引用。
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退出甚至自身釋放後,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; }
其實記住兩條便可
不調用invalidate方法,target是不會被釋放的,由於圖中的L4,L3一直存在
不許時!
對於第一種狀況咱們不該該在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點:
以我來說,歷來沒有特別準的時間,
iOS7之後,Timer 有個屬性叫作 Tolerance (時間寬容度,默認是0),標示了當時間點到後,允許有多少最大偏差。
它只會在準確的觸發時間到加上Tolerance時間內觸發,而不會提早觸發(是否是有點像咱們的火車,只會晚點。。。)。另外可重複定時器的觸發時間點不受Tolerance影響,即相似上面說的t8.5觸發後,下一個點不會是t10.5,而是t10 + Tolerance
,不讓timer由於Tolerance而產生漂移(忽然想起嵌入式使人頭疼的溫漂)。
其實對於這種不許點,對咱們開發影響並不大(基本是毫秒妙級別如下的延遲),不多會用到很是準點的狀況。
其實這種咱們平時也常常用(一次性定時):
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限制