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停止的问题,因为列表滚动的时候runloop的mode切换了,需要我们手动将timer的runloop切换为commonMode.
3.我们经常会为使用NSTimer出现的内存泄漏而烦恼,即使有解决方案,总感觉很别扭.
为什么会内存泄漏呢?我们看一下api的描述
timer会一直强引用target,直到timer调用invalide方法,在[timer invalidate]调用之前,timer和vc构成了循环引用的关系,所以在我们vc在退出当前页面的时候dealloc方法并不调用,所以不论我们的timer是strong还是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_walltime ,dispatch_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:相当于NSTimer的invalidate.
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方法。还请不吝赐教和点赞支持,谢谢。