1. 簡(jiǎn)介
??iOS常用的計(jì)時(shí)器大概有三種福澡,分別是:NSTimer叠赦、CADisplayLink、dispatch_source_t革砸。以及NSDelayedPerforming眯搭、dispatch_after兩種延時(shí)執(zhí)行的機(jī)制。本文只介紹他們基本的用法以及使用過(guò)程中注意的問(wèn)題业岁。
2. 計(jì)時(shí)器
2.1 NSTimer
NSTimir的8種系統(tǒng)初始化方法在使用過(guò)程中容易出現(xiàn)循環(huán)引用導(dǎo)致內(nèi)存泄漏的問(wèn)題鳞仙,我在這篇文章中有詳細(xì)的說(shuō)明。
關(guān)于這個(gè)問(wèn)題YYKit中做了很好的處理笔时。借助在NSTimer+YYAdd
與YYWeakProxy
我們可以輕易的規(guī)避這些問(wèn)題棍好。
2.1.1 開啟定時(shí)器
2.1.1.1 方法一
需要引入
NSTimer+YYAdd
__weak typeof(self) weakSelf = self;
_yyTimer = [NSTimer scheduledTimerWithTimeInterval:1 block:^(NSTimer * _Nonnull timer) {
NSLog(@"定時(shí)器觸發(fā), %@", weakSelf);
} repeats:YES];
2.1.1.2 方法二
需要引入
YYWeakProxy
- (void)startTimer{
//初始化代理
YYWeakProxy* wProxy = [[YYWeakProxy alloc] initWithTarget:self];
//開啟定時(shí)器
_yyTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:wProxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction {
NSLog(@"定時(shí)器觸發(fā), %@", self);
}
2.1.2 銷毀定時(shí)器
//可以在任意需要停止的時(shí)刻銷毀定時(shí)器。eg:在dealloc方法中銷毀
- (void)dealloc {
if (_yyTimer){
[_yyTimer invalidate];
_yyTimer = nil;
}
}
2.2 CADisplayLink
2.2.1 簡(jiǎn)介
CADisplayLink是一個(gè)能讓我們以和屏幕刷新率相同的頻率將內(nèi)容畫到屏幕上的定時(shí)器。我們?cè)趹?yīng)用中創(chuàng)建一個(gè)新的 CADisplayLink 對(duì)象借笙,把它添加到一個(gè)runloop中扒怖,并給它提供一個(gè) target 和 selector 在屏幕刷新的時(shí)候調(diào)用。
2.2.2 屬性說(shuō)明
duration:提供了每幀之間的時(shí)間业稼,也就是屏幕每次刷新之間的的時(shí)間盗痒。該屬性在target的selector被首次調(diào)用以后才會(huì)被賦值。selector的調(diào)用間隔時(shí)間計(jì)算方式是:時(shí)間=duration×frameInterval低散。 我們可以使用這個(gè)時(shí)間來(lái)計(jì)算出下一幀要顯示的UI的數(shù)值俯邓。但是 duration只是個(gè)大概的時(shí)間,如果CPU忙于其它計(jì)算熔号,就沒法保證以相同的頻率執(zhí)行屏幕的繪制操作稽鞭,這樣會(huì)跳過(guò)幾次調(diào)用回調(diào)方法的機(jī)會(huì)。
timestamp: 只讀的CFTimeInterval值引镊,表示屏幕顯示的上一幀的時(shí)間戳朦蕴,這個(gè)屬性通常被target用來(lái)計(jì)算下一幀中應(yīng)該顯示的內(nèi)容。 打印timestamp值弟头,其樣式類似于:179699.631584吩抓。
pause:控制CADisplayLink的運(yùn)行。當(dāng)我們想結(jié)束一個(gè)CADisplayLink的時(shí)候赴恨,應(yīng)該調(diào)用-(void)invalidate 從runloop中刪除并刪除之前綁定的 target 跟 selector疹娶。
frameInterval:是可讀可寫的NSInteger型值,標(biāo)識(shí)間隔多少幀調(diào)用一次selector 方法嘱支,默認(rèn)值是1,即每幀都調(diào)用一次挣饥。如果每幀都調(diào)用一次的話除师,對(duì)于iOS設(shè)備來(lái)說(shuō)那刷新頻率就是60HZ也就是每秒60次,如果將 frameInterval 設(shè)為2 那么就會(huì)兩幀調(diào)用一次扔枫,也就是變成了每秒刷新30次汛聚。
2.2.3 開啟定時(shí)器
- (void)startDisplayLink{
//初始化代理
YYWeakProxy* wProxy = [[YYWeakProxy alloc] initWithTarget:self];
//初始化定時(shí)器
_displayLink = [CADisplayLink displayLinkWithTarget:wProxy selector:@selector(displayLinkAction)];
//添加到 Runloop 中
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
//定時(shí)執(zhí)行方法
- (void)displayLinkAction{
}
2.2.4 銷毀定時(shí)器
- (void)dealloc {
if (_displayLink){
[_displayLink invalidate];
_displayLink = nil;
}
}
注意: CADisplayLink 不能被繼承。
2.3 CADisplayLink 與 NSTimer 的不同
2.3.1 原理不同
CADisplayLink是一個(gè)能讓我們以和屏幕刷新率同步的頻率將特定的內(nèi)容畫到屏幕上的定時(shí)器類短荐。 CADisplayLink以特定模式注冊(cè)到runloop后倚舀, 每當(dāng)屏幕顯示內(nèi)容刷新結(jié)束的時(shí)候,runloop就會(huì)向 CADisplayLink指定的target發(fā)送一次指定的selector消息忍宋, CADisplayLink類對(duì)應(yīng)的selector就會(huì)被調(diào)用一次痕貌。
NSTimer以指定的模式注冊(cè)到runloop后,每當(dāng)設(shè)定的周期時(shí)間到達(dá)后糠排,runloop會(huì)向指定的target發(fā)送一次指定的selector消息舵稠。
2.3.2 周期設(shè)置方式不同
iOS設(shè)備的屏幕刷新頻率(FPS)是60Hz,因此CADisplayLink的selector 默認(rèn)調(diào)用周期是每秒60次,這個(gè)周期可以通過(guò)frameInterval屬性設(shè)置哺徊, CADisplayLink的selector每秒調(diào)用次數(shù)=60/ frameInterval室琢。比如當(dāng) frameInterval設(shè)為2,每秒調(diào)用就變成30次落追。因此盈滴, CADisplayLink 周期的設(shè)置方式略顯不便。
NSTimer的selector調(diào)用周期可以在初始化時(shí)直接設(shè)定轿钠,相對(duì)就靈活的多巢钓。
2.3.3 精確度不同
iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會(huì)在每次刷新結(jié)束都被調(diào)用谣膳,精確度相當(dāng)高竿报。
NSTimer的精確度就顯得低了點(diǎn),比如NSTimer的觸發(fā)時(shí)間到的時(shí)候继谚,runloop如果在阻塞狀態(tài)烈菌,觸發(fā)時(shí)間就會(huì)推遲到下一個(gè)runloop周期。并且 NSTimer新增了tolerance屬性花履,讓用戶可以設(shè)置可以容忍的觸發(fā)的時(shí)間的延遲范圍芽世。
2.3.4 使用場(chǎng)景
CADisplayLink使用場(chǎng)合相對(duì)專一,適合做UI的不停重繪诡壁,比如自定義動(dòng)畫引擎或者視頻播放的渲染济瓢。
NSTimer的使用范圍要廣泛的多,各種需要單次或者循環(huán)定時(shí)處理的任務(wù)都可以使用妹卿。
2.4 dispatch_source_t
2.4.1 簡(jiǎn)介
NSTimer受runloop的影響旺矾,由于runloop需要處理很多任務(wù),導(dǎo)致NSTimer的精度降低夺克,在日常開發(fā)中箕宙,如果我們需要對(duì)定時(shí)器的精度要求很高的話,可以考慮dispatch_source_t去實(shí)現(xiàn) 铺纽。dispatch_source_t精度很高柬帕,系統(tǒng)自動(dòng)觸發(fā),系統(tǒng)級(jí)別的源狡门。
2.4.2 使用方法
//創(chuàng)建定時(shí)器
- (void)createSourceTimer{
//創(chuàng)建全局隊(duì)列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//使用全局隊(duì)列創(chuàng)建計(jì)時(shí)器
_sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設(shè)置定時(shí)器間隔時(shí)間
NSTimeInterval timeInterval = 1.0f;
//設(shè)置定時(shí)器延遲(開始)時(shí)間
NSTimeInterval delayTime = 1.0f;
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC));
//設(shè)置計(jì)時(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);
});
//銷毀定時(shí)器時(shí)執(zhí)行的 block陷寝,調(diào)用dispatch_source_cancel時(shí)觸發(fā)
dispatch_source_set_cancel_handler(_sourceTimer, ^{
NSLog(@"銷毀定時(shí)器時(shí)執(zhí)行的 block %@", weakSelf);
});
//啟動(dòng)計(jì)時(shí)器
dispatch_resume(_sourceTimer);
}
//銷毀定時(shí)器
- (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;
//取消對(duì)應(yīng)的的延遲執(zhí)行。需要注意的是參數(shù)的一致性叛复,否則無(wú)法取消
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
//取消所有的延遲執(zhí)行
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
2.5.2 使用過(guò)程中需要注意的問(wèn)題
Perform Delay 的實(shí)現(xiàn)原理就是一個(gè)不循環(huán)(repeat 為 NO)的 timer饶火,所以使用這兩個(gè)接口的注意事項(xiàng)跟使用 timer 類似鹏控。
2.5.2.1 取消時(shí)的傳參
取消對(duì)應(yīng)的的延遲執(zhí)行。需要注意的是參數(shù)的一致性肤寝,否則無(wú)法取消当辐。
//開啟延時(shí)執(zhí)行
[self performSelector:@selector(delayPerform:) withObject:@(0) afterDelay:1.0f];
//無(wú)法取消
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayPerform:) object:nil];
//可以取消
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayPerform:) object:@(1)];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
//延時(shí)執(zhí)行方法
- (void)delayPerform:(NSNumber*)param{
}
2.5.2.2 可能無(wú)法觸發(fā)
在非主線程使用的時(shí)候,需要保證線程的runloop是運(yùn)行的鲤看,否則不會(huì)執(zhí)行缘揪。或者切回主線程中使用义桂。
2.5.2.3 內(nèi)存問(wèn)題
需要在適當(dāng)?shù)牡胤秸{(diào)用取消的方法找筝,避免循環(huán)引用導(dǎo)致的內(nèi)存泄漏或者造成內(nèi)存問(wèn)題(實(shí)例都釋放了還在調(diào)用實(shí)例方法)導(dǎo)致crash。具體可以參考這篇文章慷吊,如果有更好的解決方法或者文章推薦袖裕,歡迎在評(píng)論區(qū)留言。
2.6 dispatch_after
GCD中dispatch_after方法也可以實(shí)現(xiàn)延遲溉瓶。而且不會(huì)阻塞線程急鳄,效率較高,并且可以在參數(shù)中選擇執(zhí)行的線程堰酿,但是無(wú)法取消疾宏。
//設(shè)置延時(shí)時(shí)長(zhǎng)
CGFloat delayTime = 3.f;
//開啟延時(shí)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self){
NSLog(@"定時(shí)器觸發(fā), %@", self);
}
});
注意:如果延時(shí)執(zhí)行的block還沒有執(zhí)行,當(dāng)前的控制器就 pop 的情況下触创。使用了 self 的話, 就只能在執(zhí)行了這個(gè) block 之后,當(dāng)前的 self 才能被銷毀.
2.7 UIView動(dòng)畫實(shí)現(xiàn)延時(shí)
UIView可以實(shí)現(xiàn)動(dòng)畫延遲坎藐,延時(shí)操作寫在block里面。這里需要說(shuō)明的是哼绑,block中的代碼對(duì)于是支持animation的代碼岩馍,才會(huì)有延遲效果,對(duì)于不支持animation的代碼不會(huì)有延遲效果抖韩。
[UIView animateWithDuration:1.f delay:2.f options:UIViewAnimationOptionCurveLinear animations:^{
//延時(shí)執(zhí)行的block
} completion:^(BOOL finished) {
//執(zhí)行完畢
}];
Reference
- iOS開發(fā)中深入理解CADisplayLink和NSTimer
- iOS-OC定時(shí)器大總結(jié)(NSTimer蛀恩、performSelector、GCD帽蝶、dispatch_source_t赦肋、CADisplayLink)
- 封裝一個(gè)GCD定時(shí)器块攒,徹底解決定時(shí)器循環(huán)引用励稳、釋放時(shí)機(jī)問(wèn)題
- AsyncSocket 源碼分析
- 定時(shí)器集合 NSTimer & CADisplayLink & dispatch_source_t & dispatch_after & NSDelayedPerforming