金三銀四,祝大家能找到滿意的工作~
話不多說,進(jìn)入正題
定時器相信大家肯定不會陌生,iOS中常用的定時器有三種,分別是NSTimer,CADisplayLink和GCD。
NSTimer
兩種方式創(chuàng)建
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(test) userInfo:nil repeats:YES];
// 停止定時器
[timer invalidate];
timer == nil
創(chuàng)建方式2
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(test) userInfo:nil repeats:YES];
// 將定時器添加到runloop中,否則定時器不會啟動
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 停止定時器
[timer invalidate];
timer == nil
方式1會自動將創(chuàng)建的定時器以默認(rèn)方式添加到當(dāng)前線程runloop中,而無需手動添加。但是在此種模式下,當(dāng)滾動屏幕時runloop會進(jìn)入另外一種模式,定時器會暫停,為了解決這種問題,可以像方式2那樣把定時器添加到NSRunLoopCommonModes模式下。
方式1和方式2在設(shè)置后都會在間隔設(shè)定的時間(本例中設(shè)置為2s)后執(zhí)行test方法,如果需要立即執(zhí)行可以使用下面的代碼。
[time fire];
不過,NSTimer相對來說是不精確的,參考蘋果官方文檔介紹timer
咳咳,鳥語,筆者大致翻譯了一下重點內(nèi)容??
NSTimer 不是一個基于真實時間的機(jī)制。NSTimer被激發(fā)需要滿足三個條件,
1.NSTimer被添加到特定mode的runloop中;
2.該mode型的runloop正在運(yùn)行;
3.到達(dá)激發(fā)時間。因為一個run loop需要管理大量的輸入源,為了提NSTimer的效率,時間間隔限制為50-100毫秒比較合理。如果一個NSTimer的激發(fā)時間
出現(xiàn)在一個耗時的方法中,或者當(dāng)前run loop的mode沒有監(jiān)測該NSTimer,那么定時器就不會被激發(fā),直到下一次run loop檢測到該NSTimer時才會激發(fā)。
因此,NSTimer的實際激發(fā)時間很有可能會比規(guī)劃時間延后一段時間。
哎,再來這樣解釋一下
NSTimer導(dǎo)致誤差的原因:
1.NSTimer加在main runloop中,模式是NSDefaultRunLoopMode,main負(fù)責(zé)所有主線程事件,例如UI界面的操作,復(fù)雜的運(yùn)算,這樣在同一個runloop中timer就會產(chǎn)生阻塞。
2.模式的改變。主線程的 RunLoop 里有兩個預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。
當(dāng)你創(chuàng)建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復(fù)回調(diào),但此時滑動一個ScrollView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調(diào),并且也不會影響到滑動操作。所以就會影響到NSTimer不準(zhǔn)的情況。
PS:DefaultMode 是 App 平時所處的狀態(tài),rackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態(tài)。
解決辦法
方案1.在主線程中進(jìn)行NSTimer操作,但是將NSTimer實例加到main runloop的特定mode(模式)中。避免被復(fù)雜運(yùn)算操作或者UI界面刷新所干擾。
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
方案2.在子線程中進(jìn)行NSTimer的操作,再在主線程中修改UI界面顯示操作結(jié)果;
- (void)timer2 {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
}
- (void)newThread {
@autoreleasepool {
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(showTime) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}
CADisplayLink
// 創(chuàng)建displayLink
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(test:)];
// 將創(chuàng)建的displaylink添加到runloop中,否則定時器不會執(zhí)行
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
// 停止定時器
[displayLink invalidate];
displayLink = nil;
當(dāng)把CADisplayLink對象add到runloop中后,selector就能被周期性調(diào)用,類似于重復(fù)的NSTimer被啟動了;執(zhí)行invalidate操作時,CADisplayLink對象就會從runloop中移除,selector調(diào)用也隨即停止,類似于NSTimer的invalidate方法
需要注意的地方
調(diào)用時機(jī)
CADisplayLink是一個和屏幕刷新率同步的定時器類。CADisplayLink以特定模式注冊到runloop后,每當(dāng)屏幕顯示內(nèi)容刷新結(jié)束的時候,runloop就會向CADisplayLink指定的target發(fā)送一次指定的selector消息,CADisplayLink類對應(yīng)的selector就會被調(diào)用一次,所以可以使用CADisplayLink做一些和屏幕操作相關(guān)的操作。
重要屬性
1.frameInterval
NSInteger類型的值,用來設(shè)置間隔多少幀調(diào)用一次selector方法,默認(rèn)值是1,即每幀都調(diào)用一次。
2.duration
readOnly的CFTimeInterval值,表示兩次屏幕刷新之間的時間間隔。需要注意的是,該屬性在target的selector被首次調(diào)用以后才會被賦值。selector的調(diào)用間隔時間計算方式是:調(diào)用間隔時間 = duration × frameInterval。
3.timestamp
只讀的CFTimeInterval值,表示屏幕顯示的上一幀的時間戳,這個屬性通常被target用來計算下一幀中應(yīng)該顯示的內(nèi)容。
CADisplayLink注意點總結(jié)
注意點:
iOS并不能保證能以每秒60次的頻率調(diào)用回調(diào)方法,這取決于:
1、CPU的空閑程度
如果CPU忙于其它計算,就沒法保證以60HZ執(zhí)行屏幕的繪制動作,導(dǎo)致跳過若干次調(diào)用回調(diào)方法的機(jī)會,跳過次數(shù)取決CPU的忙碌程度。
2、執(zhí)行回調(diào)方法所用的時間
如果執(zhí)行回調(diào)時間大于重繪每幀的間隔時間,就會導(dǎo)致跳過若干次回調(diào)調(diào)用機(jī)會,這取決于執(zhí)行時間長短。
總結(jié):
從原理上不難看出,CADisplayLink使用場合相對專一,適合做界面的不停重繪,比如視頻播放的時候需要不停地獲取下一幀用于界面渲染。
GCD定時器
一次性定時
dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC);
dispatch_after(timer, dispatch_get_main_queue(), ^(void){
NSLog(@"GCD-----%@",[NSThread currentThread]);
});
重復(fù)執(zhí)行的定時器
@property (nonatomic ,strong)dispatch_source_t timer;// 注意:此處應(yīng)該使用強(qiáng)引用 strong
{
//0.創(chuàng)建隊列
dispatch_queue_t queue = dispatch_get_main_queue();
//1.創(chuàng)建GCD中的定時器
/*
第一個參數(shù):創(chuàng)建source的類型 DISPATCH_SOURCE_TYPE_TIMER:定時器
第二個參數(shù):0
第三個參數(shù):0
第四個參數(shù):隊列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//2.設(shè)置時間等
/*
第一個參數(shù):定時器對象
第二個參數(shù):DISPATCH_TIME_NOW 表示從現(xiàn)在開始計時
第三個參數(shù):間隔時間 GCD里面的時間最小單位為 納秒
第四個參數(shù):精準(zhǔn)度(表示允許的誤差,0表示絕對精準(zhǔn))
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.要調(diào)用的任務(wù)
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCD-----%@",[NSThread currentThread]);
});
//4.開始執(zhí)行
dispatch_resume(timer);
//
self.timer = timer;
}
注意的地方: 此處注意一定要強(qiáng)引用定時器 ,否則定時器執(zhí)行到 } 后將會被釋放,無定時效果。GCD定時器時間非常精準(zhǔn),最小的定時時間可以達(dá)到1納秒,所以用在非常精確的定時場合。
補(bǔ)充一下
NSObject的方法也有類似功能的方法
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
敲黑板總結(jié)
1.NSTimer和performSelector必須保證有一個活躍的runloop。
performSelector和scheduledTimerWithTimeInterval方法都是基于runloop的。我們知道,當(dāng)一個應(yīng)用啟動時,系統(tǒng)會開啟一個主線程,并且把主線程的runloop激活,也就是run起來,并且主線程的runloop是不會停止的。所以,當(dāng)這兩個方法在主線程可以被正常調(diào)用。但情況往往不是這樣的。實際編碼中,我們更多的邏輯是放在子線程中執(zhí)行的。而子線程的runloop是默認(rèn)關(guān)閉的。這時如果不手動激活runloop,performSelector和scheduledTimerWithTimeInterval的調(diào)用將是無效的。
2.NSTimer的創(chuàng)建與撤銷必須在同一個線程操作、performSelector的創(chuàng)建與撤銷必須在同一個線程操作。
3.內(nèi)存管理有潛在泄露的風(fēng)險
scheduledTimerWithTimeInterval方法將target設(shè)為A對象時,A對象會被這個timer所持有,也就是會被retain一次,timer會被當(dāng)前的runloop所持有。performSelector:withObject:afterDelay:方法實際上是在當(dāng)前線程的runloop里幫你創(chuàng)建的一個timer去執(zhí)行任務(wù),所以和scheduledTimerWithTimeInterval方法一樣會retain其調(diào)用對象。但是,我們往往不希望因為這些延遲操作而影響對象的生命周期,更甚至是,導(dǎo)致對象無法釋放。
4.CADisplayLink
iOS設(shè)備的屏幕刷新頻率(FPS)是60Hz,因此CADisplayLink的selector默認(rèn)調(diào)用周期是每秒60次,這個周期可以通過frameInterval屬性設(shè)置,CADisplayLink的selector每秒調(diào)用次數(shù)=60/frameInterval。比如當(dāng)frameInterval設(shè)為2,每秒調(diào)用就變成30次。因此,CADisplayLink周期的設(shè)置方式略顯不便。
NSTimer的selector調(diào)用周期可以在初始化時直接設(shè)定,相對就靈活的多。
iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會在每次刷新結(jié)束都被調(diào)用,精確度相當(dāng)高。
NSTimer的精確度就顯得低了點,比如NSTimer的觸發(fā)時間到的時候,runloop如果在忙于別的調(diào)用,觸發(fā)時間就會推遲到下一個runloop周期。更有甚者,在OS X v10.9以后為了盡量避免在NSTimer觸發(fā)時間到了而去中斷當(dāng)前處理的任務(wù),NSTimer新增了tolerance屬性,讓用戶可以設(shè)置可以容忍的觸發(fā)的時間范圍。
5.GCD
若使用dispatch_after,系統(tǒng)會幫我們處理線程級的邏輯,這樣也我們更易于享受系統(tǒng)對線程所做的優(yōu)化。除此之外,我們不用關(guān)心runloop的問題。并且調(diào)用的對象也不會被強(qiáng)行持有,這樣上述的內(nèi)存問題也不復(fù)存在。當(dāng)然,需要注意block會持有其傳入的對象,但這可以通過weakself解決。所以在這種延遲操作方案中,使用dispatch_after更佳。
但是呢,dispatch_after有個致命的弱點:dispatch_after一旦執(zhí)行后,就不能撤銷了。而performSelector可以使用cancelPreviousPerformRequestsWithTarget方法撤銷,NSTimer也可以調(diào)用invalidate進(jìn)行撤銷。(注意:撤銷任務(wù)與創(chuàng)建timer任務(wù)必須在同一個線程,即同一個runloop)所以我們還是得用NSTimer或者performSelector嗎?
NO,其實GCD也有timer的功能。
筆者自己封裝了利用GCD實現(xiàn)的定時器,大家可以拿去直接使用。
end...