金三銀四,祝大家能找到滿意的工作~
話不多說(shuō),進(jìn)入正題
定時(shí)器相信大家肯定不會(huì)陌生儡遮,iOS中常用的定時(shí)器有三種蔫慧,分別是NSTimer,CADisplayLink和GCD。
NSTimer
兩種方式創(chuàng)建
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(test) userInfo:nil repeats:YES];
// 停止定時(shí)器
[timer invalidate];
timer == nil
創(chuàng)建方式2
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(test) userInfo:nil repeats:YES];
// 將定時(shí)器添加到runloop中,否則定時(shí)器不會(huì)啟動(dòng)
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 停止定時(shí)器
[timer invalidate];
timer == nil
方式1會(huì)自動(dòng)將創(chuàng)建的定時(shí)器以默認(rèn)方式添加到當(dāng)前線程runloop中,而無(wú)需手動(dòng)添加蛤虐。但是在此種模式下,當(dāng)滾動(dòng)屏幕時(shí)runloop會(huì)進(jìn)入另外一種模式肝陪,定時(shí)器會(huì)暫停驳庭,為了解決這種問(wèn)題,可以像方式2那樣把定時(shí)器添加到NSRunLoopCommonModes模式下氯窍。
方式1和方式2在設(shè)置后都會(huì)在間隔設(shè)定的時(shí)間(本例中設(shè)置為2s)后執(zhí)行test方法饲常,如果需要立即執(zhí)行可以使用下面的代碼。
[time fire];
不過(guò)狼讨,NSTimer相對(duì)來(lái)說(shuō)是不精確的贝淤,參考蘋(píng)果官方文檔介紹timer
咳咳,鳥(niǎo)語(yǔ)政供,筆者大致翻譯了一下重點(diǎn)內(nèi)容??
NSTimer 不是一個(gè)基于真實(shí)時(shí)間的機(jī)制播聪。NSTimer被激發(fā)需要滿足三個(gè)條件,
1.NSTimer被添加到特定mode的runloop中布隔;
2.該mode型的runloop正在運(yùn)行离陶;
3.到達(dá)激發(fā)時(shí)間。因?yàn)橐粋€(gè)run loop需要管理大量的輸入源执泰,為了提NSTimer的效率枕磁,時(shí)間間隔限制為50-100毫秒比較合理渡蜻。如果一個(gè)NSTimer的激發(fā)時(shí)間
出現(xiàn)在一個(gè)耗時(shí)的方法中术吝,或者當(dāng)前run loop的mode沒(méi)有監(jiān)測(cè)該NSTimer,那么定時(shí)器就不會(huì)被激發(fā)茸苇,直到下一次run loop檢測(cè)到該NSTimer時(shí)才會(huì)激發(fā)排苍。
因此,NSTimer的實(shí)際激發(fā)時(shí)間很有可能會(huì)比規(guī)劃時(shí)間延后一段時(shí)間。
哎学密,再來(lái)這樣解釋一下
NSTimer導(dǎo)致誤差的原因:
1.NSTimer加在main runloop中淘衙,模式是NSDefaultRunLoopMode,main負(fù)責(zé)所有主線程事件腻暮,例如UI界面的操作彤守,復(fù)雜的運(yùn)算毯侦,這樣在同一個(gè)runloop中timer就會(huì)產(chǎn)生阻塞。
2.模式的改變具垫。主線程的 RunLoop 里有兩個(gè)預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode侈离。
當(dāng)你創(chuàng)建一個(gè) Timer 并加到 DefaultMode 時(shí),Timer 會(huì)得到重復(fù)回調(diào)筝蚕,但此時(shí)滑動(dòng)一個(gè)ScrollView時(shí)卦碾,RunLoop 會(huì)將 mode 切換為 TrackingRunLoopMode,這時(shí) Timer 就不會(huì)被回調(diào)起宽,并且也不會(huì)影響到滑動(dòng)操作洲胖。所以就會(huì)影響到NSTimer不準(zhǔn)的情況。
PS:DefaultMode 是 App 平時(shí)所處的狀態(tài)坯沪,rackingRunLoopMode 是追蹤 ScrollView 滑動(dòng)時(shí)的狀態(tài)绿映。
解決辦法
方案1.在主線程中進(jìn)行NSTimer操作,但是將NSTimer實(shí)例加到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中卸奉,否則定時(shí)器不會(huì)執(zhí)行
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
// 停止定時(shí)器
[displayLink invalidate];
displayLink = nil;
當(dāng)把CADisplayLink對(duì)象add到runloop中后,selector就能被周期性調(diào)用颖御,類似于重復(fù)的NSTimer被啟動(dòng)了榄棵;執(zhí)行invalidate操作時(shí),CADisplayLink對(duì)象就會(huì)從runloop中移除潘拱,selector調(diào)用也隨即停止疹鳄,類似于NSTimer的invalidate方法
需要注意的地方
調(diào)用時(shí)機(jī)
CADisplayLink是一個(gè)和屏幕刷新率同步的定時(shí)器類。CADisplayLink以特定模式注冊(cè)到runloop后芦岂,每當(dāng)屏幕顯示內(nèi)容刷新結(jié)束的時(shí)候瘪弓,runloop就會(huì)向CADisplayLink指定的target發(fā)送一次指定的selector消息,CADisplayLink類對(duì)應(yīng)的selector就會(huì)被調(diào)用一次禽最,所以可以使用CADisplayLink做一些和屏幕操作相關(guān)的操作腺怯。
重要屬性
1.frameInterval
NSInteger類型的值,用來(lái)設(shè)置間隔多少幀調(diào)用一次selector方法川无,默認(rèn)值是1呛占,即每幀都調(diào)用一次。
2.duration
readOnly的CFTimeInterval值懦趋,表示兩次屏幕刷新之間的時(shí)間間隔晾虑。需要注意的是,該屬性在target的selector被首次調(diào)用以后才會(huì)被賦值。selector的調(diào)用間隔時(shí)間計(jì)算方式是:調(diào)用間隔時(shí)間 = duration × frameInterval帜篇。
3.timestamp
只讀的CFTimeInterval值糙捺,表示屏幕顯示的上一幀的時(shí)間戳,這個(gè)屬性通常被target用來(lái)計(jì)算下一幀中應(yīng)該顯示的內(nèi)容笙隙。
CADisplayLink注意點(diǎn)總結(jié)
注意點(diǎn):
iOS并不能保證能以每秒60次的頻率調(diào)用回調(diào)方法继找,這取決于:
1、CPU的空閑程度
如果CPU忙于其它計(jì)算逃沿,就沒(méi)法保證以60HZ執(zhí)行屏幕的繪制動(dòng)作婴渡,導(dǎo)致跳過(guò)若干次調(diào)用回調(diào)方法的機(jī)會(huì),跳過(guò)次數(shù)取決CPU的忙碌程度凯亮。
2边臼、執(zhí)行回調(diào)方法所用的時(shí)間
如果執(zhí)行回調(diào)時(shí)間大于重繪每幀的間隔時(shí)間,就會(huì)導(dǎo)致跳過(guò)若干次回調(diào)調(diào)用機(jī)會(huì)假消,這取決于執(zhí)行時(shí)間長(zhǎng)短柠并。
總結(jié):
從原理上不難看出,CADisplayLink使用場(chǎng)合相對(duì)專一富拗,適合做界面的不停重繪臼予,比如視頻播放的時(shí)候需要不停地獲取下一幀用于界面渲染。
GCD定時(shí)器
一次性定時(shí)
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í)行的定時(shí)器
@property (nonatomic ,strong)dispatch_source_t timer;// 注意:此處應(yīng)該使用強(qiáng)引用 strong
{
//0.創(chuàng)建隊(duì)列
dispatch_queue_t queue = dispatch_get_main_queue();
//1.創(chuàng)建GCD中的定時(shí)器
/*
第一個(gè)參數(shù):創(chuàng)建source的類型 DISPATCH_SOURCE_TYPE_TIMER:定時(shí)器
第二個(gè)參數(shù):0
第三個(gè)參數(shù):0
第四個(gè)參數(shù):隊(duì)列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//2.設(shè)置時(shí)間等
/*
第一個(gè)參數(shù):定時(shí)器對(duì)象
第二個(gè)參數(shù):DISPATCH_TIME_NOW 表示從現(xiàn)在開(kāi)始計(jì)時(shí)
第三個(gè)參數(shù):間隔時(shí)間 GCD里面的時(shí)間最小單位為 納秒
第四個(gè)參數(shù):精準(zhǔn)度(表示允許的誤差,0表示絕對(duì)精準(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.開(kāi)始執(zhí)行
dispatch_resume(timer);
//
self.timer = timer;
}
注意的地方: 此處注意一定要強(qiáng)引用定時(shí)器 啃沪,否則定時(shí)器執(zhí)行到 } 后將會(huì)被釋放粘拾,無(wú)定時(shí)效果。GCD定時(shí)器時(shí)間非常精準(zhǔn)创千,最小的定時(shí)時(shí)間可以達(dá)到1納秒缰雇,所以用在非常精確的定時(shí)場(chǎng)合。
補(bǔ)充一下
NSObject的方法也有類似功能的方法
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
敲黑板總結(jié)
1.NSTimer和performSelector必須保證有一個(gè)活躍的runloop追驴。
performSelector和scheduledTimerWithTimeInterval方法都是基于runloop的械哟。我們知道,當(dāng)一個(gè)應(yīng)用啟動(dòng)時(shí)殿雪,系統(tǒng)會(huì)開(kāi)啟一個(gè)主線程暇咆,并且把主線程的runloop激活,也就是run起來(lái)丙曙,并且主線程的runloop是不會(huì)停止的爸业。所以,當(dāng)這兩個(gè)方法在主線程可以被正常調(diào)用河泳。但情況往往不是這樣的沃呢。實(shí)際編碼中,我們更多的邏輯是放在子線程中執(zhí)行的拆挥。而子線程的runloop是默認(rèn)關(guān)閉的。這時(shí)如果不手動(dòng)激活runloop,performSelector和scheduledTimerWithTimeInterval的調(diào)用將是無(wú)效的纸兔。
2.NSTimer的創(chuàng)建與撤銷必須在同一個(gè)線程操作惰瓜、performSelector的創(chuàng)建與撤銷必須在同一個(gè)線程操作。
3.內(nèi)存管理有潛在泄露的風(fēng)險(xiǎn)
scheduledTimerWithTimeInterval方法將target設(shè)為A對(duì)象時(shí)汉矿,A對(duì)象會(huì)被這個(gè)timer所持有崎坊,也就是會(huì)被retain一次,timer會(huì)被當(dāng)前的runloop所持有洲拇。performSelector:withObject:afterDelay:方法實(shí)際上是在當(dāng)前線程的runloop里幫你創(chuàng)建的一個(gè)timer去執(zhí)行任務(wù)奈揍,所以和scheduledTimerWithTimeInterval方法一樣會(huì)retain其調(diào)用對(duì)象。但是赋续,我們往往不希望因?yàn)檫@些延遲操作而影響對(duì)象的生命周期男翰,更甚至是,導(dǎo)致對(duì)象無(wú)法釋放纽乱。
4.CADisplayLink
iOS設(shè)備的屏幕刷新頻率(FPS)是60Hz蛾绎,因此CADisplayLink的selector默認(rèn)調(diào)用周期是每秒60次,這個(gè)周期可以通過(guò)frameInterval屬性設(shè)置鸦列,CADisplayLink的selector每秒調(diào)用次數(shù)=60/frameInterval租冠。比如當(dāng)frameInterval設(shè)為2,每秒調(diào)用就變成30次薯嗤。因此顽爹,CADisplayLink周期的設(shè)置方式略顯不便。
NSTimer的selector調(diào)用周期可以在初始化時(shí)直接設(shè)定骆姐,相對(duì)就靈活的多话原。
iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會(huì)在每次刷新結(jié)束都被調(diào)用诲锹,精確度相當(dāng)高繁仁。
NSTimer的精確度就顯得低了點(diǎn),比如NSTimer的觸發(fā)時(shí)間到的時(shí)候归园,runloop如果在忙于別的調(diào)用黄虱,觸發(fā)時(shí)間就會(huì)推遲到下一個(gè)runloop周期。更有甚者庸诱,在OS X v10.9以后為了盡量避免在NSTimer觸發(fā)時(shí)間到了而去中斷當(dāng)前處理的任務(wù)捻浦,NSTimer新增了tolerance屬性,讓用戶可以設(shè)置可以容忍的觸發(fā)的時(shí)間范圍桥爽。
5.GCD
若使用dispatch_after朱灿,系統(tǒng)會(huì)幫我們處理線程級(jí)的邏輯,這樣也我們更易于享受系統(tǒng)對(duì)線程所做的優(yōu)化钠四。除此之外盗扒,我們不用關(guān)心runloop的問(wèn)題跪楞。并且調(diào)用的對(duì)象也不會(huì)被強(qiáng)行持有,這樣上述的內(nèi)存問(wèn)題也不復(fù)存在侣灶。當(dāng)然甸祭,需要注意block會(huì)持有其傳入的對(duì)象,但這可以通過(guò)weakself解決褥影。所以在這種延遲操作方案中池户,使用dispatch_after更佳。
但是呢凡怎,dispatch_after有個(gè)致命的弱點(diǎn):dispatch_after一旦執(zhí)行后校焦,就不能撤銷了。而performSelector可以使用cancelPreviousPerformRequestsWithTarget方法撤銷统倒,NSTimer也可以調(diào)用invalidate進(jìn)行撤銷寨典。(注意:撤銷任務(wù)與創(chuàng)建timer任務(wù)必須在同一個(gè)線程,即同一個(gè)runloop)所以我們還是得用NSTimer或者performSelector嗎檐薯?
NO凝赛,其實(shí)GCD也有timer的功能。
筆者自己封裝了利用GCD實(shí)現(xiàn)的定時(shí)器坛缕,大家可以拿去直接使用墓猎。
end...