1. 簡介
??iOS常用的計時器大概有三種,分別是:NSTimer、CADisplayLink、dispatch_source_t。以及NSDelayedPerforming、dispatch_after兩種延時執(zhí)行的機制。本文只介紹他們基本的用法以及使用過程中注意的問題。
2. 計時器
2.1 NSTimer
NSTimir的8種系統(tǒng)初始化方法在使用過程中容易出現(xiàn)循環(huán)引用導(dǎo)致內(nèi)存泄漏的問題,我在這篇文章中有詳細的說明。
關(guān)于這個問題YYKit中做了很好的處理。借助在NSTimer+YYAdd與YYWeakProxy 我們可以輕易的規(guī)避這些問題。
2.1.1 開啟定時器
2.1.1.1 方法一
需要引入
NSTimer+YYAdd
__weak typeof(self) weakSelf = self;
_yyTimer = [NSTimer scheduledTimerWithTimeInterval:1 block:^(NSTimer * _Nonnull timer) {
NSLog(@"定時器觸發(fā), %@", weakSelf);
} repeats:YES];
2.1.1.2 方法二
需要引入
YYWeakProxy
- (void)startTimer{
//初始化代理
YYWeakProxy* wProxy = [[YYWeakProxy alloc] initWithTarget:self];
//開啟定時器
_yyTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:wProxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction {
NSLog(@"定時器觸發(fā), %@", self);
}
2.1.2 銷毀定時器
//可以在任意需要停止的時刻銷毀定時器。eg:在dealloc方法中銷毀
- (void)dealloc {
if (_yyTimer){
[_yyTimer invalidate];
_yyTimer = nil;
}
}
2.2 CADisplayLink
2.2.1 簡介
CADisplayLink是一個能讓我們以和屏幕刷新率相同的頻率將內(nèi)容畫到屏幕上的定時器。我們在應(yīng)用中創(chuàng)建一個新的 CADisplayLink 對象,把它添加到一個runloop中,并給它提供一個 target 和 selector 在屏幕刷新的時候調(diào)用。
2.2.2 屬性說明
duration:提供了每幀之間的時間,也就是屏幕每次刷新之間的的時間。該屬性在target的selector被首次調(diào)用以后才會被賦值。selector的調(diào)用間隔時間計算方式是:時間=duration×frameInterval。 我們可以使用這個時間來計算出下一幀要顯示的UI的數(shù)值。但是 duration只是個大概的時間,如果CPU忙于其它計算,就沒法保證以相同的頻率執(zhí)行屏幕的繪制操作,這樣會跳過幾次調(diào)用回調(diào)方法的機會。
timestamp: 只讀的CFTimeInterval值,表示屏幕顯示的上一幀的時間戳,這個屬性通常被target用來計算下一幀中應(yīng)該顯示的內(nèi)容。 打印timestamp值,其樣式類似于:179699.631584。
pause:控制CADisplayLink的運行。當(dāng)我們想結(jié)束一個CADisplayLink的時候,應(yīng)該調(diào)用-(void)invalidate 從runloop中刪除并刪除之前綁定的 target 跟 selector。
frameInterval:是可讀可寫的NSInteger型值,標(biāo)識間隔多少幀調(diào)用一次selector 方法,默認(rèn)值是1,即每幀都調(diào)用一次。如果每幀都調(diào)用一次的話,對于iOS設(shè)備來說那刷新頻率就是60HZ也就是每秒60次,如果將 frameInterval 設(shè)為2 那么就會兩幀調(diào)用一次,也就是變成了每秒刷新30次。
2.2.3 開啟定時器
- (void)startDisplayLink{
//初始化代理
YYWeakProxy* wProxy = [[YYWeakProxy alloc] initWithTarget:self];
//初始化定時器
_displayLink = [CADisplayLink displayLinkWithTarget:wProxy selector:@selector(displayLinkAction)];
//添加到 Runloop 中
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
//定時執(zhí)行方法
- (void)displayLinkAction{
}
2.2.4 銷毀定時器
- (void)dealloc {
if (_displayLink){
[_displayLink invalidate];
_displayLink = nil;
}
}
注意: CADisplayLink 不能被繼承。
2.3 CADisplayLink 與 NSTimer 的不同
2.3.1 原理不同
CADisplayLink是一個能讓我們以和屏幕刷新率同步的頻率將特定的內(nèi)容畫到屏幕上的定時器類。 CADisplayLink以特定模式注冊到runloop后, 每當(dāng)屏幕顯示內(nèi)容刷新結(jié)束的時候,runloop就會向 CADisplayLink指定的target發(fā)送一次指定的selector消息, CADisplayLink類對應(yīng)的selector就會被調(diào)用一次。
NSTimer以指定的模式注冊到runloop后,每當(dāng)設(shè)定的周期時間到達后,runloop會向指定的target發(fā)送一次指定的selector消息。
2.3.2 周期設(shè)置方式不同
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è)定,相對就靈活的多。
2.3.3 精確度不同
iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會在每次刷新結(jié)束都被調(diào)用,精確度相當(dāng)高。
NSTimer的精確度就顯得低了點,比如NSTimer的觸發(fā)時間到的時候,runloop如果在阻塞狀態(tài),觸發(fā)時間就會推遲到下一個runloop周期。并且 NSTimer新增了tolerance屬性,讓用戶可以設(shè)置可以容忍的觸發(fā)的時間的延遲范圍。
2.3.4 使用場景
CADisplayLink使用場合相對專一,適合做UI的不停重繪,比如自定義動畫引擎或者視頻播放的渲染。
NSTimer的使用范圍要廣泛的多,各種需要單次或者循環(huán)定時處理的任務(wù)都可以使用。
2.4 dispatch_source_t
2.4.1 簡介
NSTimer受runloop的影響,由于runloop需要處理很多任務(wù),導(dǎo)致NSTimer的精度降低,在日常開發(fā)中,如果我們需要對定時器的精度要求很高的話,可以考慮dispatch_source_t去實現(xiàn) 。dispatch_source_t精度很高,系統(tǒng)自動觸發(fā),系統(tǒng)級別的源。
2.4.2 使用方法
//創(chuàng)建定時器
- (void)createSourceTimer{
//創(chuàng)建全局隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//使用全局隊列創(chuàng)建計時器
_sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設(shè)置定時器間隔時間
NSTimeInterval timeInterval = 1.0f;
//設(shè)置定時器延遲(開始)時間
NSTimeInterval delayTime = 1.0f;
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC));
//設(shè)置計時器
dispatch_source_set_timer(_sourceTimer,startDelayTime,timeInterval*NSEC_PER_SEC,0.1*NSEC_PER_SEC);
//定期執(zhí)行事件
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(_sourceTimer,^{
NSLog(@"定期執(zhí)行的 block %@", weakSelf);
});
//銷毀定時器時執(zhí)行的 block,調(diào)用dispatch_source_cancel時觸發(fā)
dispatch_source_set_cancel_handler(_sourceTimer, ^{
NSLog(@"銷毀定時器時執(zhí)行的 block %@", weakSelf);
});
//啟動計時器
dispatch_resume(_sourceTimer);
}
//銷毀定時器
- (void)destoryTimer{
dispatch_source_cancel(_sourceTimer);
}
2.4.3 封裝拓展
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
YYTimer is a thread-safe timer based on GCD. It has similar API with `NSTimer`.
YYTimer object differ from NSTimer in a few ways:
* It use GCD to produce timer tick, and won't be affected by runLoop.
* It make a weak reference to the target, so it can avoid retain cycles.
* It always fire on main thread.
*/
@interface YYTimer : NSObject
+ (YYTimer *)timerWithTimeInterval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats;
- (instancetype)initWithFireTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats NS_DESIGNATED_INITIALIZER;
@property (readonly) BOOL repeats;
@property (readonly) NSTimeInterval timeInterval;
@property (readonly, getter=isValid) BOOL valid;
- (void)invalidate;
- (void)fire;
@end
#import "YYTimer.h"
#import <pthread.h>
#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(_lock);
@implementation YYTimer {
BOOL _valid;
NSTimeInterval _timeInterval;
BOOL _repeats;
__weak id _target;
SEL _selector;
dispatch_source_t _source;
dispatch_semaphore_t _lock;
}
+ (YYTimer *)timerWithTimeInterval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats {
return [[self alloc] initWithFireTime:interval interval:interval target:target selector:selector repeats:repeats];
}
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYTimer init error" reason:@"Use the designated initializer to init." userInfo:nil];
return [self initWithFireTime:0 interval:0 target:self selector:@selector(invalidate) repeats:NO];
}
- (instancetype)initWithFireTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats {
self = [super init];
_repeats = repeats;
_timeInterval = interval;
_valid = YES;
_target = target;
_selector = selector;
__weak typeof(self) _self = self;
_lock = dispatch_semaphore_create(1);
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_source, dispatch_time(DISPATCH_TIME_NOW, (start * NSEC_PER_SEC)), (interval * NSEC_PER_SEC), 0);
dispatch_source_set_event_handler(_source, ^{[_self fire];});
dispatch_resume(_source);
return self;
}
- (void)invalidate {
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
if (_valid) {
dispatch_source_cancel(_source);
_source = NULL;
_target = nil;
_valid = NO;
}
dispatch_semaphore_signal(_lock);
}
- (void)fire {
if (!_valid) return;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
id target = _target;
if (!target) {
dispatch_semaphore_signal(_lock);
[self invalidate];
} else {
dispatch_semaphore_signal(_lock);
[target performSelector:_selector withObject:self];
if (!_repeats) {
[self invalidate];
}
}
#pragma clang diagnostic pop
}
- (BOOL)repeats {
LOCK(BOOL repeat = _repeats); return repeat;
}
- (NSTimeInterval)timeInterval {
LOCK(NSTimeInterval t = _timeInterval) return t;
}
- (BOOL)isValid {
LOCK(BOOL valid = _valid) return valid;
}
- (void)dealloc {
[self invalidate];
}
@end
2.5 NSDelayedPerforming
2.5.1 使用方法
//設(shè)置延遲執(zhí)行,delay單位為秒
//在指定的某些mode下
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
//在當(dāng)前mode下
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
//取消對應(yīng)的的延遲執(zhí)行。需要注意的是參數(shù)的一致性,否則無法取消
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
//取消所有的延遲執(zhí)行
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
2.5.2 使用過程中需要注意的問題
Perform Delay 的實現(xiàn)原理就是一個不循環(huán)(repeat 為 NO)的 timer,所以使用這兩個接口的注意事項跟使用 timer 類似。
2.5.2.1 取消時的傳參
取消對應(yīng)的的延遲執(zhí)行。需要注意的是參數(shù)的一致性,否則無法取消。
//開啟延時執(zhí)行
[self performSelector:@selector(delayPerform:) withObject:@(0) afterDelay:1.0f];
//無法取消
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayPerform:) object:nil];
//可以取消
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayPerform:) object:@(1)];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
//延時執(zhí)行方法
- (void)delayPerform:(NSNumber*)param{
}
2.5.2.2 可能無法觸發(fā)
在非主線程使用的時候,需要保證線程的runloop是運行的,否則不會執(zhí)行?;蛘咔谢刂骶€程中使用。
2.5.2.3 內(nèi)存問題
需要在適當(dāng)?shù)牡胤秸{(diào)用取消的方法,避免循環(huán)引用導(dǎo)致的內(nèi)存泄漏或者造成內(nèi)存問題(實例都釋放了還在調(diào)用實例方法)導(dǎo)致crash。具體可以參考這篇文章,如果有更好的解決方法或者文章推薦,歡迎在評論區(qū)留言。
2.6 dispatch_after
GCD中dispatch_after方法也可以實現(xiàn)延遲。而且不會阻塞線程,效率較高,并且可以在參數(shù)中選擇執(zhí)行的線程,但是無法取消。
//設(shè)置延時時長
CGFloat delayTime = 3.f;
//開啟延時
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self){
NSLog(@"定時器觸發(fā), %@", self);
}
});
注意:如果延時執(zhí)行的block還沒有執(zhí)行,當(dāng)前的控制器就 pop 的情況下。使用了 self 的話, 就只能在執(zhí)行了這個 block 之后,當(dāng)前的 self 才能被銷毀.
2.7 UIView動畫實現(xiàn)延時
UIView可以實現(xiàn)動畫延遲,延時操作寫在block里面。這里需要說明的是,block中的代碼對于是支持animation的代碼,才會有延遲效果,對于不支持animation的代碼不會有延遲效果。
[UIView animateWithDuration:1.f delay:2.f options:UIViewAnimationOptionCurveLinear animations:^{
//延時執(zhí)行的block
} completion:^(BOOL finished) {
//執(zhí)行完畢
}];