dispatch_source之自定义Timer

6,022 阅读5分钟

1、How about NSTimer?

NSTimer可能大家都熟悉,他的api也都很简单,但是其使用过程并不容易,相信用过的同学都踩过坑.通常我们这么用:

// 定义

@property (nonatomic, strong) NSTimer *timer;
// 使用

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(demo:) userInfo:nil repeats:YES];

1. timerWithTimeInterval开头的方法需要自己添加到指定的runloop中去,而scheduledTimerWithTimeInterval开头的方法默认添加到当前的runloop中去.

2. 默认添加到runloop中的NSTimer会以NSDefaultRunLoopMode的模式放入当前的runloop,这就导致经常出现的列表滚动timer停止的问题,因为列表滚动的时候runloopmode切换了,需要我们手动将timerrunloop切换为commonMode.

3.我们经常会为使用NSTimer出现的内存泄漏而烦恼,即使有解决方案,总感觉很别扭.

为什么会内存泄漏呢?我们看一下api的描述

timer会一直强引用target,直到timer调用invalide方法,在[timer invalidate]调用之前,timervc构成了循环引用的关系,所以在我们vc在退出当前页面的时候dealloc方法并不调用,所以不论我们的timerstrong还是weak都无济于事,因为api内部会强持有。

网上有很多种方案去实现打破这种循环引用来解决内存泄漏问题,这里我们从自定义timer说起,自定义timer说到底就是用dispatch_source这套api去实现.

2、Dispatch Source Timer

Dispatch Source Timer 是一种与dispatch queue结合使用的定时器,当需要在后台queue中定期执行任务的时候,使用dispatch source timer 要比使用NSTimer更自然,更高效(因为无需再main queue 和 异步queue之间切换)。

2.1 创建timer

官方文档提供创建timer的示例是:

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;
}

注意点:

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.

3、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方法。还请不吝赐教和点赞支持,谢谢。