本文旨在總結(jié)項目中因使用各類定時器而踩到的坑,并附上經(jīng)驗總結(jié)。
-
NSTimer
NSTimer是最常用的定時器,坑也最多。總結(jié)如下:
- NSTimer的精度
NSTimer是不精確的,如果不考慮線程阻塞,設(shè)置的時間間隔NSTimeInterval
在秒級別情況下精度還可以接受,一旦到達毫秒級,就會有明顯誤差。
而如果在資源有限的機器上,如腎4,由于線程阻塞,定時器會高概率漏過repeat
點,導(dǎo)致計時出現(xiàn)累計誤差。也就是說,一旦線程阻塞, NSTimer錯過了這個重復(fù)點,那它就真的錯過了,不會去補做。
我們可以斷言:NSTimer是否精確,很大程度上取決于線程當(dāng)前的空閑情況。
- NSTimer與RunLoop
通常情況下,我們在主線程中運行方法,NSTimer在創(chuàng)建后被
默認添加在RunLoop的NSDefaultRunLoopMode
模式上。這里有兩個坑:
坑1:主線程的RunLoop是默認打開的,你不用自己手動去執(zhí)行[[NSRunLoop currentRunLoop] run];
。但是如果你在后臺線程開啟定時器,比如:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(repeat:)
userInfo:nil}
repeats:YES];
[[NSRunLoop currentRunLoop] run];
});```
這句話就要加上去了,否則NSTimer不會被添加到后臺線程的Runloop中,也就不會執(zhí)行。
此外,根據(jù)規(guī)范,你在哪里開啟了定時器,就應(yīng)該在哪里終止它,因此上述代碼,請在后臺線程中調(diào)用終止方法```- (void)invalidate;```。
坑2: 是關(guān)于RunLoop模式的,NSTimer
默認添加在```NSDefaultRunLoopMode```。而我們的一些控件,如```UIScrollView```滑動的時候主線程的Runloop會怒切模式到```UITrackingRunLoopMode```,此時NSTimer就不會執(zhí)行,看起來就像暫停了,這樣還何談計時的精確?不過解決方法很簡單,只要將NSTimer添加到```NSRunLoopCommonModes```上即可。```NSRunLoopCommonModes```是所有RunLoop模式的集合,自然可以處理上述兩種情況。
* ###CADisplayLink
它是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣,其內(nèi)部實際是操作了一個 Source)。可能有人覺得它會精確一點,但經(jīng)過實際測試,同樣會和NSTimer一樣會有誤差。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù),那其中就會有一幀被跳過去。
* ###GCD中的Timer
目測這個是較為準確的,使用Dispatch Source來實現(xiàn)。具體可見這篇文章:[iOS: NSTimer使用小記](https://www.mgenware.com/blog/?p=459)。
* ###一些補充技巧
1.定時器計時不準確的簡便解決方案
如果你的代碼是類似于這樣的:
static long timeCount = 0; //初始化計時count
...; //sth
timeCount++;//進入定時器repeat點
那么就會存在丟幀引起的計數(shù)不準確。
可以使用```CADisplayLink```獲取當(dāng)前時間,在下一次刷新幀之后,得出兩幀的時間差,以此來累加,合理的代碼可以寫成這樣:
self.lastTime = CACurrentMediaTime();
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(repeat:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
......; //sth
- (void)repeat:(CADisplayLink *)timer
{
//calculate time delta
CFTimeInterval currentTime = CACurrentMediaTime();
CFTimeInterval timeDuration = currentTime - self.lastTime;
self.lastTime = currentTime;
//update time offset
self.timeCount +=timeDuration;
}
2.RunLoop的模式選擇注意點
* ```NSDefaultRunLoopMode```:標準優(yōu)先級
* ```NSRunLoopCommonModes```:高優(yōu)先級
* ```UITrackingRunLoopMode```:用于```UIScrollView```和別的控件的動畫
在上文中有說到,如果使用```NSDefaultRunLoopMode```,是不能保定時器平滑運行的,所以就可以用```NSRunLoopCommonModes```
來替代。但是要小心,因為如果這個定時器在一個高幀率情況下運行,你會發(fā)現(xiàn)一些別的基于定時器的任務(wù),或者其他類似于滑動的iOS動畫會暫停,直到這個定時器任務(wù)結(jié)束。
我們可以同時加入```NSDefaultRunLoopMode```和```UITrackingRunLoopMode```來保證它不會被滑動打斷,也不會影響其他UIKit控件的性能,就像這樣:
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:UITrackingRunLoopMode];
最后,希望這篇小結(jié)能夠幫助到大家,避免踩坑。: )
# 微信公眾號
第一時間獲取最新內(nèi)容,歡迎關(guān)注微信公眾號:「洛斯里克的大書庫」。
