dispatch_source之自定義Timer

一、How about NSTimer?

NSTimer可能你們都熟悉,他的api也都很簡單,可是其使用過程並不容易,相信用過的同窗都踩過坑.一般咱們這麼用:ios

// 定義 
@property (nonatomic, strong) NSTimer *timer;
// 使用 
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(demo:) userInfo:nil repeats:YES];
複製代碼

1. timerWithTimeInterval開頭的方法須要本身添加到指定的runloop中去,而scheduledTimerWithTimeInterval開頭的方法默認添加到當前的runloop中去.git

2. 默認添加到runloop中的NSTimer會以NSDefaultRunLoopMode的模式放入當前的runloop,這就致使常常出現的列表滾動timer中止的問題,由於列表滾動的時候runloopmode切換了,須要咱們手動將timerrunloop切換爲commonMode.github

3.咱們常常會爲使用NSTimer出現的內存泄漏而煩惱,即便有解決方案,總感受很彆扭.api

爲何會內存泄漏呢?咱們看一下api的描述markdown

timer會一直強引用target,直到timer調用invalide方法,在[timer invalidate]調用以前,timervc構成了循環引用的關係,因此在咱們vc在退出當前頁面的時候dealloc方法並不調用,因此不論咱們的timerstrong仍是weak都無濟於事,由於api內部會強持有。app

網上有不少種方案去實現打破這種循環引用來解決內存泄漏問題,這裏咱們從自定義timer提及,自定義timer說到底就是用dispatch_source這套api去實現.異步

二、Dispatch Source Timer

Dispatch Source Timer 是一種與dispatch queue結合使用的定時器,當須要在後臺queue中按期執行任務的時候,使用dispatch source timer 要比使用NSTimer更天然,更高效(由於無需再main queue 和 異步queue之間切換)。ide

2.1 建立timer

官方文檔提供建立timer的示例是:oop

dispatch_source_t CreateDispatchTimer(uint64_t interval,
              uint64_t leeway,
              dispatch_queue_t queue,
              dispatch_block_t block)
{
   dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                                     0, 0, queue);
   if (timer)
   {
      dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
      dispatch_source_set_event_handler(timer, block);
      dispatch_resume(timer);
   }
   return timer;
}
複製代碼

注意點:ui

1. 此處的timer是間隔定時器,每隔一段時間就會觸發而不須要像NSTimer那樣設值repeats.

2. dispatch_source_set_timer 中的第二個參數能夠傳入dispatch_time 或者 dispatch_walltimedispatch_time表示選擇是默認鐘錶來計時,這個會隨着系統休眠而休眠,而 dispatch_walltime 可讓定時間按照實際時間一直運行下去,因此針對間隔時間比較久的定時器應採用dispatch_walltime,上述例子中的dispatch_walltime(NULL, 0) 等同於 dispatch_time(DISPATCH_WALLTIME_NOW, 0).

3. dispatch_source_set_timer 中的第四個參數leeway 表明的是指望容忍時間,若是設值爲1,意味着定時器時間達到前一秒或者後一秒才真正觸發定時器,計時指定leeway的值爲0,系統也沒法保證徹底精確的觸發時間,只是儘量的知足這個需求.

4. dispatch_source_set_event_handler 的參數block表明的是定時器間隔要執行的任務,它是綁定在執行的queue上的,這個相比NSTimer要方便太多,因爲NSTimer須要Runloop支持,NSTimer則須要手動添加到指定線程的runloop中去才能執行.

2.2開啓 和 中止timer

開啓timer的方法:

  • dispatch_resume

中止Dispatch Timer有兩種方法:

  • dispatch_suspend :只是暫時把timer掛起,須要和dispatch_resume配對使用,在掛起期間,產生的事件會積累,等到resume的時候會整合爲一個事件發送.
  • dispatch_source_cancel:至關於NSTimerinvalidate.

三、Custom Timer

結合上述的理解,咱們嘗試自定義timer,一樣咱們模仿NSTimer的接口,這樣使用起來沒有違和感也更方便:

@interface SSTimer : NSObject
 /// 同下面的方法,不過自動開始執行
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
 /// 建立一個定時器並返回,可是並不會自動執行,須要手動調用resume方法
 /// - parameter: start 定時器啓動時間
 /// - parameter: ti 間隔多久開始執行selector
 /// - parameter: s 執行的任務
 /// - parameter: ui 綁定信息
 /// - parameter: rep 是否重複
- (instancetype)initWithTimeInterval:(NSTimeInterval)start interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
 /// 擴充block
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(SSTimer *timer))block;
 /// 啓動
- (void)resume;
 /// 暫定
- (void)suspend;
 /// 關閉
- (void)invalidate;
@property (readonly) BOOL repeats;
@property (readonly) NSTimeInterval timeInterval;
@property (readonly, getter=isValid) BOOL valid;
@property (nullable, readonly, retain) id userInfo;
@end
複製代碼

這裏咱們提供了啓動和暫停的功能,相比NSTimer要好用不少,同時也擴充了block的參數,雖然apple也提供了block的參數方法,可是須要在ios10以上的系統上才能使用。

#import "SSTimer.h"
 #define lock(...) \
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);\
    __VA_ARGS__;\
    dispatch_semaphore_signal(_semaphore);
 @implementation SSTimer {
    BOOL _valid;
    NSTimeInterval _timeInterval;
    BOOL _repeats;
    __weak id _target;
    SEL _selector;
    dispatch_source_t _timer;
    dispatch_semaphore_t _semaphore;
    id _userInfo;
    BOOL _running;
}
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    SSTimer *timer = [[SSTimer alloc] initWithTimeInterval:0 interval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo];
    [timer resume];
    return timer;
}
+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(SSTimer *timer))block {
    NSParameterAssert(block != nil);
    SSTimer *timer = [[SSTimer alloc] initWithTimeInterval:0 interval:interval target:self selector:@selector(ss_executeBlockFromTimer:) userInfo:[block copy] repeats:repeats];
    [timer resume];
    return timer;
}
- (instancetype)initWithTimeInterval:(NSTimeInterval)start interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep {
    self = [super init];
    if (self) {
        _valid = YES;
        _timeInterval = ti;
        _repeats = rep;
        _target = t;
        _selector = s;
        _userInfo = ui;
        _semaphore = dispatch_semaphore_create(1);
        __weak typeof(self) weakSelf = self;
        _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), ti * NSEC_PER_SEC, 0);
        dispatch_source_set_event_handler(_timer, ^{[weakSelf fire];});
    }
    return self;
}
- (void)fire {
    if (!_valid) {return;}
    lock(id target = _target;)
    if (!target) {
        [self invalidate];
    } else {
        // 執行selector
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [target performSelector:_selector withObject:self];
#pragma clang diagnostic pop
        if (!_repeats) {
            [self invalidate];
        }
    }
}
- (void)resume {
    if (_running) return;
    dispatch_resume(_timer);
    _running = YES;
}
- (void)suspend {
    if (!_running) return;
    dispatch_suspend(_timer);
    _running = NO;
}
- (void)invalidate {
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    if (_valid) {
        dispatch_source_cancel(_timer);
        _timer = NULL;
        _target = nil;
        _userInfo = nil;
        _valid = NO;
    }
    dispatch_semaphore_signal(_semaphore);
}
- (id)userInfo {
    lock(id ui = _userInfo) return ui;
}
- (BOOL)repeats {
    lock(BOOL re = _repeats) return re;
}
- (NSTimeInterval)timeInterval {
    lock(NSTimeInterval ti = _timeInterval) return ti;
}
- (BOOL)isValid {
    lock(BOOL va = _valid) return va;
}
- (void)dealloc {
    [self invalidate];
}
+ (void)ss_executeBlockFromTimer:(SSTimer *)aTimer {
    void (^block)(SSTimer *) = [aTimer userInfo];
    if (block) block(aTimer);
}
@end
複製代碼

這裏咱們使用了__weak id _target,這樣咱們就內部就切斷了這層循環引用問題,作到自主釋放,外層使用方也沒必要關心,也不容易出現錯誤和內存泄漏. 具體的代碼在SSTimer下載,須要的能夠前去查看,目前內部也只是定義在main queue上執行任務,後續會添加關於不一樣queue上執行任務的timer方法。還請不吝賜教和點贊支持,謝謝。

相關文章
相關標籤/搜索