定時器,用來延遲或重復執行某些方法,例如:網絡定時刷新,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就是實現動畫效果的倚天屠龍。