深入學習iOS定時器

定時器,用來延遲或重復執行某些方法,例如:網絡定時刷新,UI間隔刷新,動畫效果......iOS中的定時器大致分為這幾類:
<pre>
NSObject
GCD定時器
NSTimer
CADisplayLink
</pre>

RunLoop

在講解定時器之前,先普及下RunLoop的基本知識。傳送門: iOS - RunLoop 深入理解感謝ibireme整理了一份完整講解,從 CFRunLoop 的源碼入手,介紹 RunLoop 的概念以及底層實現原理。在此,總結性的介紹下。

RunLoop概念

一般來講,一個線程一次只能執行一個任務,執行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:

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

這種模型通常被稱作 Event Loop。 Event Loop 在很多系統和框架里都有實現,比如 Node.js 的事件處理,比如 Windows 程序的消息循環,再比如 OSX/iOS 里的 RunLoop。實現這種模型的關鍵點在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。所以,RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數后,就會一直處于這個函數內部 "接受消息->等待->處理" 的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回。
蘋果不允許直接創建 RunLoop,需要類似像主線程調用一樣調用,即[NSRunLoop mainRunLoop],在 CoreFoundation 里面關于 RunLoop 有5個類:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其中CFRunLoopTimerRef 是基于時間的觸發器,其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。后面要講的NSTimer 其實就是 CFRunLoopTimerRef。

NSObject

iOS框架圖
在object-c中,絕大部分類的基類都是NSObject,使用NSObject延遲執行也被用于網絡定時刷新,配套使用代碼cancelPreviousPerformRequestsWithTarget與performSelector,相對而言比較簡潔。當然NSObject與RunLoop之間的聯系遠不只于此,例如事件響應和手勢識別,就不做展開。

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

當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
當調用 cancelPreviousPerformRequestsWithTarget時,實際上就是講Timer 從RunLoop中移除。

GCD

GCD定時器其實是一種特殊的分派源,它是基于分派隊列的,而NSTimer是基于運行循環的,所以,尤其是在多線程中,GCD定時器要比NSTimer好用的多。另外,GCD定時器使用dispatch_block_t,而不是方法選擇器,也就是說GCD實現的定時器是不受RunLoop約束。
實際上 RunLoop 底層也會用到 GCD 的東西,在<pre>CFRrunLoop.c</pre>中我們能發現引用了<pre>#include <dispatch/dispatch.h></pre>,例如RunLoop 的超時時間就是使用 GCD 中的 dispatch_source_t來實現的,摘自 __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) { //超時時間在最大限制內,才創建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中有詳細講解。
但同時 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執行這個 block。

GCD定時器實現:

  • 執行一次
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
});
  • 重復執行
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時已經提到過:NSTimer 其實就是 CFRunLoopTimerRef。他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節省資源,并不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。
如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延后執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

使用方法:

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

其中,
TimerInterval : 執行之前等待的時間。比如設置成1.0,就代表1秒后執行方法
target : 需要執行方法的對象。
selector : 需要執行的方法
repeats : 是否需要循環

  • 結束方法
[timer invalidate];

CADisplayLink

簡單地說,CADisplayLink就是一個定時器,保持跟屏幕刷新率相同的頻率刷新。
雖然CADisplayLink使用場合相對專一,只適合做UI的不停重繪,但并不妨礙他成為很多高手熱愛的技巧之一。在做精細的動畫效果時,CADisplayLink將是一個很好的助手,例如自定義動畫引擎或者視頻播放的渲染;類似于siri語音輸入效果就用到了CADisplayLink;很多模仿wave效果也多采用CADisplayLink刷新界面。
iOS設備的屏幕刷新頻率是固定的,我們在使用時不用關心屏幕的刷新頻率,因為它本身就是跟屏幕刷新同步的。CADisplayLink在正常情況下會在每次刷新結束都被調用,精確度相當高。

屬性

  • timestamp:只讀,屏幕顯示的上一幀的時間戳,timestamp = duration * frameInterval。
  • duration:只讀,系統屏幕每次刷新的時間戳,在target的selector被首次調用以后被系統賦值。
  • targetTimestamp:只讀
  • paused:讀寫,控制CADisplayLink的運行,即暫停操作。結束一個CADisplayLink實例需要調用invalidate從runloop刪除
  • frameInterval:讀寫,標識間隔多少幀調用一次selector 方法,默認為1,即每幀都調用一次。對于iOS設備來說那刷新頻率就是60HZ也就是每秒60次,如果將 frameInterval 設為2 那么就會兩幀調用一次,也就是變成了每秒刷新30次。
  • preferredFramesPerSecond:

使用方法:

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

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

如果說CADisplayLink是控制動畫流暢度的尚方寶劍,那UIBezierPath與CAShapeLayer就是實現動畫效果的倚天屠龍。

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

推薦閱讀更多精彩內容