iOS定時器NSTimer、CADisplayLink、dispatch_source_t以及延時方法的使用

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

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;
//取消對應(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í)行完畢
}];

Reference

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容