深入學(xué)習(xí)iOS定時器

定時器,用來延遲或重復(fù)執(zhí)行某些方法店枣,例如:網(wǎng)絡(luò)定時刷新,UI間隔刷新,動畫效果......iOS中的定時器大致分為這幾類:
<pre>
NSObject
GCD定時器
NSTimer
CADisplayLink
</pre>

RunLoop

在講解定時器之前,先普及下RunLoop的基本知識。傳送門: iOS - RunLoop 深入理解感謝ibireme整理了一份完整講解作郭,從 CFRunLoop 的源碼入手割捅,介紹 RunLoop 的概念以及底層實(shí)現(xiàn)原理。在此忙灼,總結(jié)性的介紹下。

RunLoop概念

一般來講,一個線程一次只能執(zhí)行一個任務(wù)棍弄,執(zhí)行完成后線程就會退出。如果我們需要一個機(jī)制疟游,讓線程能隨時處理事件但并不退出照卦,通常的代碼邏輯是這樣的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實(shí)現(xiàn)乡摹,比如 Node.js 的事件處理役耕,比如 Windows 程序的消息循環(huán),再比如 OSX/iOS 里的 RunLoop聪廉。實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息瞬痘,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒板熊。所以框全,RunLoop 實(shí)際上就是一個對象,這個對象管理了其需要處理的事件和消息干签,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯津辩。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中容劳,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息)喘沿,函數(shù)返回。
蘋果不允許直接創(chuàng)建 RunLoop竭贩,需要類似像主線程調(diào)用一樣調(diào)用蚜印,即[NSRunLoop mainRunLoop],在 CoreFoundation 里面關(guān)于 RunLoop 有5個類:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其中CFRunLoopTimerRef 是基于時間的觸發(fā)器留量,其包含一個時間長度和一個回調(diào)(函數(shù)指針)窄赋。當(dāng)其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點(diǎn)楼熄,當(dāng)時間點(diǎn)到時忆绰,RunLoop會被喚醒以執(zhí)行那個回調(diào)。后面要講的NSTimer 其實(shí)就是 CFRunLoopTimerRef可岂。

NSObject

iOS框架圖
在object-c中错敢,絕大部分類的基類都是NSObject,使用NSObject延遲執(zhí)行也被用于網(wǎng)絡(luò)定時刷新青柄,配套使用代碼cancelPreviousPerformRequestsWithTarget與performSelector伐债,相對而言比較簡潔预侯。當(dāng)然NSObject與RunLoop之間的聯(lián)系遠(yuǎn)不只于此,例如事件響應(yīng)和手勢識別峰锁,就不做展開萎馅。

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;

當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實(shí)際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當(dāng)前線程的 RunLoop 中虹蒋。所以如果當(dāng)前線程沒有 RunLoop糜芳,則這個方法會失效。
當(dāng)調(diào)用 performSelector:onThread: 時魄衅,實(shí)際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去峭竣,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會失效晃虫。
當(dāng)調(diào)用 cancelPreviousPerformRequestsWithTarget時皆撩,實(shí)際上就是講Timer 從RunLoop中移除。

GCD

GCD定時器其實(shí)是一種特殊的分派源哲银,它是基于分派隊(duì)列的扛吞,而NSTimer是基于運(yùn)行循環(huán)的,所以荆责,尤其是在多線程中滥比,GCD定時器要比NSTimer好用的多。另外做院,GCD定時器使用dispatch_block_t盲泛,而不是方法選擇器,也就是說GCD實(shí)現(xiàn)的定時器是不受RunLoop約束键耕。
實(shí)際上 RunLoop 底層也會用到 GCD 的東西寺滚,在<pre>CFRrunLoop.c</pre>中我們能發(fā)現(xiàn)引用了<pre>#include <dispatch/dispatch.h></pre>,例如RunLoop 的超時時間就是使用 GCD 中的 dispatch_source_t來實(shí)現(xiàn)的郁竟,摘自 __CFRunLoopRun中的源碼:

 dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) { //超時時間在最大限制內(nèi)玛迄,才創(chuàng)建timeout_timer
        dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
            dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }

GCD API 記錄 (三)中的 dispatch_source中的timer中有詳細(xì)講解由境。
但同時 GCD 提供的某些接口也用到了 RunLoop棚亩, 例如 dispatch_async()。當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時虏杰,libDispatch 會向主線程的 RunLoop 發(fā)送消息讥蟆,RunLoop會被喚醒,并從消息中取得這個 block纺阔,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執(zhí)行這個 block瘸彤。

GCD定時器實(shí)現(xiàn):

  • 執(zhí)行一次
double delayTimer = 1.0;    
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayTimer * NSEC_PER_SEC);   
 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 
          //do
});
  • 重復(fù)執(zhí)行
NSTimeInterval delayTimer = 1.0;     
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);    dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), delayTimer * NSEC_PER_SEC, 0);     
dispatch_source_set_event_handler(_timer, ^{   
       //do    
});
 dispatch_resume(_timer);

NSTimer

在介紹RunLoop時已經(jīng)提到過:NSTimer 其實(shí)就是 CFRunLoopTimerRef。他們之間是 toll-free bridged 的笛钝。一個 NSTimer 注冊到 RunLoop 后质况,RunLoop 會為其重復(fù)的時間點(diǎn)注冊好事件愕宋。例如 10:00, 10:10, 10:20 這幾個時間點(diǎn)。RunLoop為了節(jié)省資源结榄,并不會在非常準(zhǔn)確的時間點(diǎn)回調(diào)這個Timer中贝。Timer 有個屬性叫做 Tolerance (寬容度),標(biāo)示了當(dāng)時間點(diǎn)到后臼朗,容許有多少最大誤差邻寿。
如果某個時間點(diǎn)被錯過了,例如執(zhí)行了一個很長的任務(wù)视哑,則那個時間點(diǎn)的回調(diào)也會跳過去绣否,不會延后執(zhí)行。就比如等公交挡毅,如果 10:10 時我忙著玩手機(jī)錯過了那個點(diǎn)的公交蒜撮,那我只能等 10:20 這一趟了。

使用方法:

  • 創(chuàng)建方法
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

其中跪呈,
TimerInterval : 執(zhí)行之前等待的時間淀弹。比如設(shè)置成1.0,就代表1秒后執(zhí)行方法
target : 需要執(zhí)行方法的對象庆械。
selector : 需要執(zhí)行的方法
repeats : 是否需要循環(huán)

  • 結(jié)束方法
[timer invalidate];

CADisplayLink

簡單地說薇溃,CADisplayLink就是一個定時器,保持跟屏幕刷新率相同的頻率刷新缭乘。
雖然CADisplayLink使用場合相對專一沐序,只適合做UI的不停重繪,但并不妨礙他成為很多高手熱愛的技巧之一堕绩。在做精細(xì)的動畫效果時策幼,CADisplayLink將是一個很好的助手,例如自定義動畫引擎或者視頻播放的渲染奴紧;類似于siri語音輸入效果就用到了CADisplayLink特姐;很多模仿wave效果也多采用CADisplayLink刷新界面。
iOS設(shè)備的屏幕刷新頻率是固定的黍氮,我們在使用時不用關(guān)心屏幕的刷新頻率唐含,因?yàn)樗旧砭褪歉聊凰⑿峦降摹ADisplayLink在正常情況下會在每次刷新結(jié)束都被調(diào)用沫浆,精確度相當(dāng)高捷枯。

屬性

  • timestamp:只讀,屏幕顯示的上一幀的時間戳专执,timestamp = duration * frameInterval淮捆。
  • duration:只讀,系統(tǒng)屏幕每次刷新的時間戳,在target的selector被首次調(diào)用以后被系統(tǒng)賦值攀痊。
  • targetTimestamp:只讀
  • paused:讀寫桐腌,控制CADisplayLink的運(yùn)行,即暫停操作苟径。結(jié)束一個CADisplayLink實(shí)例需要調(diào)用invalidate從runloop刪除
  • frameInterval:讀寫哩掺,標(biāo)識間隔多少幀調(diào)用一次selector 方法,默認(rèn)為1涩笤,即每幀都調(diào)用一次嚼吞。對于iOS設(shè)備來說那刷新頻率就是60HZ也就是每秒60次,如果將 frameInterval 設(shè)為2 那么就會兩幀調(diào)用一次蹬碧,也就是變成了每秒刷新30次舱禽。
  • preferredFramesPerSecond:

使用方法:

  • 創(chuàng)建方法
  CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(wave)];
    [timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
  • 結(jié)束方法
[timer invalidate];

衍生:CADisplayLink與UIBezierPath、CAShapeLayer的激情碰撞

如果說CADisplayLink是控制動畫流暢度的尚方寶劍恩沽,那UIBezierPath與CAShapeLayer就是實(shí)現(xiàn)動畫效果的倚天屠龍誊稚。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市罗心,隨后出現(xiàn)的幾起案子里伯,更是在濱河造成了極大的恐慌,老刑警劉巖渤闷,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疾瓮,死亡現(xiàn)場離奇詭異,居然都是意外死亡飒箭,警方通過查閱死者的電腦和手機(jī)狼电,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弦蹂,“玉大人肩碟,你說我怎么就攤上這事⊥勾唬” “怎么了削祈?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長脑漫。 經(jīng)常有香客問我髓抑,道長,這世上最難降的妖魔是什么窿撬? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任启昧,我火速辦了婚禮,結(jié)果婚禮上劈伴,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好跛璧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布严里。 她就那樣靜靜地躺著,像睡著了一般追城。 火紅的嫁衣襯著肌膚如雪刹碾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天座柱,我揣著相機(jī)與錄音迷帜,去河邊找鬼。 笑死色洞,一個胖子當(dāng)著我的面吹牛戏锹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播火诸,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锦针,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了置蜀?” 一聲冷哼從身側(cè)響起奈搜,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盯荤,沒想到半個月后馋吗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秋秤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年耗美,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片航缀。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡商架,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出芥玉,到底是詐尸還是另有隱情蛇摸,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布灿巧,位于F島的核電站赶袄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏抠藕。R本人自食惡果不足惜饿肺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盾似。 院中可真熱鬧敬辣,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至撰茎,卻和暖如春嵌牺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背龄糊。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工逆粹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炫惩。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓僻弹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诡必。 傳聞我的和親對象是個殘疾皇子奢方,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

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