iOS面試總結(jié)——精確定時器

金三銀四,祝大家能找到滿意的工作~

話不多說,進(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...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內(nèi)容

  • 最近看了很多RunLoop的文章,看完很懵逼,決心整理一下,文章中大部分內(nèi)容都是引用大神們的,但好歹對自己有個交代...
    小涼介閱讀 6,733評論 12 79
  • 一. NSTimer NSTimer的初始化方法有以下幾種: 會自動啟動, 并加入* MainRunloop*的*...
    codeshow閱讀 512評論 0 0
  • 定時器,用來延遲或重復(fù)執(zhí)行某些方法,例如:網(wǎng)絡(luò)定時刷新,UI間隔刷新,動畫效果......iOS中的定時器大致分為...
    sweetpf閱讀 739評論 1 2
  • 1 Runloop機(jī)制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi閱讀 4,048評論 4 30
  • That'?s a love story,can you hear it? Warsaw Station 500路...
    Russland閱讀 508評論 0 0