iOS定時(shí)器NSTimer逞怨、CADisplayLink、dispatch_source_t以及延時(shí)方法的使用

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+YYAddYYWeakProxy 我們可以輕易的規(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 封裝拓展

YY大神的YYTimer已經(jīng)拓展的比較全面了。這里貼出源碼以供學(xué)習(xí)借鑒其馏。

#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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市囱井,隨后出現(xiàn)的幾起案子驹尼,更是在濱河造成了極大的恐慌,老刑警劉巖庞呕,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件新翎,死亡現(xiàn)場(chǎng)離奇詭異程帕,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)地啰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門愁拭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人亏吝,你說(shuō)我怎么就攤上這事岭埠。” “怎么了蔚鸥?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵惜论,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我止喷,道長(zhǎng)馆类,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任弹谁,我火速辦了婚禮乾巧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘僵闯。我一直安慰自己卧抗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布鳖粟。 她就那樣靜靜地躺著社裆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪向图。 梳的紋絲不亂的頭發(fā)上泳秀,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音榄攀,去河邊找鬼嗜傅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛檩赢,可吹牛的內(nèi)容都是我干的吕嘀。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼贞瞒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼偶房!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起军浆,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤棕洋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后乒融,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掰盘,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡摄悯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了愧捕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奢驯。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖次绘,靈堂內(nèi)的尸體忽然破棺而出叨橱,到底是詐尸還是另有隱情,我是刑警寧澤断盛,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布罗洗,位于F島的核電站,受9級(jí)特大地震影響钢猛,放射性物質(zhì)發(fā)生泄漏伙菜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一命迈、第九天 我趴在偏房一處隱蔽的房頂上張望贩绕。 院中可真熱鬧,春花似錦壶愤、人聲如沸淑倾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)娇哆。三九已至,卻和暖如春勃救,著一層夾襖步出監(jiān)牢的瞬間碍讨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工蒙秒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勃黍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓晕讲,卻偏偏與公主長(zhǎng)得像覆获,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瓢省,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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