iOS中有三种定时器,NSTimer、CADisplayLink以及GCD。因为NSTimer和CADisplayLink都是依赖于Runloop的,所以如果Runloop任务多,可能会导致计时的不准确,所以通常情况下我们建议使用GCD的定时器
特点
- GCD定时器不依赖NSRunLoop,他是一个基于苹果内核的独立体系
- 精度高,最小到1纳秒
- 没有invalidate方法
- 不需要手动管理内存(这里封装的target方式一定要注意循环引用)
一般写法
@interface ZXKGCDTimerVC ()
@property (nonatomic, strong) dispatch_source_t gcd_timer;
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSTimeInterval start = 0.0;//开始时间
NSTimeInterval interval = 1.0;//时间间隔
/*
这里需要一个队列
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
/*
第一个参数:定时器对象
第二个参数:DISPATCH_TIME_NOW 表示从现在开始计时,start * NSEC_PER_SEC表示从现在开始经过start秒执行
第三个参数:间隔时间 GCD里面的时间最小单位为 interval * NSEC_PER_SEC表示间隔为interval秒
第四个参数:精准度(表示允许的误差,0表示绝对精准)
*/
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"------");
});
self.gcd_timer = timer;
dispatch_resume(self.gcd_timer);
}
- (void)dealloc {
NSLog(@"%s", __func__);
dispatch_source_cancel(self.gcd_timer);
}
封装
GCD的定时器写法相对比较固定,这里建议封装,封装之前先理一理思路(GCD定时器的封装必然依赖原有的写法,所以原有的一些关键性的参数必须保留):
1、开始时间
2、时间间隔
3、是否需要重复执行
4、是否支持多线程
5、具体执行任务的函数或者block,其实这里两种都是可以的
6、取消定时任务
上述就是简单的思路,接下来先进行简单的封装,如下所示:
- 初步封装
@interface ZXKGCDTimer : NSObject
+ (void)timerTask:(void(^)(void))task
start:(NSTimeInterval) start
interval:(NSTimeInterval) interval
repeats:(BOOL) repeats
async:(BOOL)async;
+ (void)canelTimer;
@end
@implementation ZXKGCDTimer
+ (void)timerTask:(void(^)(void))task
start:(NSTimeInterval) start
interval:(NSTimeInterval) interval
repeats:(BOOL) repeats
async:(BOOL)async{
/**
对参数做一些限制
1.如果task不存在,那就没有执行的必要(!task)
2.开始时间必须大于当前时间
3.当需要重复执行时,重复间隔时间必须 >0
以上条件必须满足,定时器才算是比较合理,否则没必要执行
*/
if (!task || start < 0 || (interval <= 0 && repeats)) {
return;
}
//if (!task || start < 0 || (interval <= 0 && repeats)) return; (上面的代码有人可能会写成这样,都一样,这是if的语法,里面只有一行时候可以省略{},其他的没区别)
/**
队列
async:YES 全局队列 dispatch_get_global_queue(0, 0) 可以简单理解为其他线程(非主线程)
async:NO 主队列 dispatch_get_main_queue() 可以理解为主线程
*/
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
/**
创建定时器 dispatch_source_t timer
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
//定时任务
task();
//如果不需要重复,执行一次即可
if (!repeats) {
dispatch_source_cancel(timer);
}
});
//启动定时器
dispatch_resume(timer);
}
+ (void)canelTimer {
}
@end
- 进一步封装
当我们想取消定时器的时候会思考,多个定时器如何取消呢(很多时候可能会创建不止一个定时器),得需要一个标准,或者说根据一个标记取到对应的定时器,这里我们根据key - value的形式进行完善上述定时器,具体如下:
@implementation ZXKGCDTimer
// 用来存放多个计时器的字典
static NSMutableDictionary *timers_;
/**
load 与 initialize区别,这里选用initialize
*/
+(void)initialize{
//GCD一次性函数
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString*)timerTask:(void(^)(void))task
start:(NSTimeInterval) start
interval:(NSTimeInterval) interval
repeats:(BOOL) repeats
async:(BOOL)async{
/**
对参数做一些限制
1.如果task不存在,那就没有执行的必要(!task)
2.开始时间必须大于当前时间
3.当需要重复执行时,重复间隔时间必须 >0
以上条件必须满足,定时器才算是比较合理,否则没必要执行
*/
if (!task || start < 0 || (interval <= 0 && repeats)) {
return nil;
}
//if (!task || start < 0 || (interval <= 0 && repeats)) return nil; (上面的代码有人可能会写成这样,都一样,这是if的语法,里面只有一行时候可以省略{},其他的没区别)
/**
队列
async:YES 全局队列 dispatch_get_global_queue(0, 0) 可以简单理解为其他线程(非主线程)
async:NO 主队列 dispatch_get_main_queue() 可以理解为主线程
*/
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
/**
创建定时器 dispatch_source_t timer
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 定时器的唯一标识
NSString *timerName = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[timerName] = timer;
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
//定时任务
task();
//如果不需要重复,执行一次即可
if (!repeats) {
[self canelTimer:timerName];
}
});
//启动定时器
dispatch_resume(timer);
return timerName;
}
+(void)canelTimer:(NSString*) timerName{
if (timerName.length == 0) {
return;
}
dispatch_source_t timer = timers_[timerName];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:timerName];
}
}
@end
- 最终封装
上面的代码涉及到对字典的存取,所以还需要考虑到线程安全(只需要在存值取值的时候进行处理),因此,需要进一步完善代码,此外我们还加入了暂停和继续的方法,特别注意,suspend必须与resume成对存在,不然就会发生崩溃,而且我们不只是以block的方式进行封装,当然了,也可以使用target的方式,相对比较简单,具体如下:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ZXKGCDTimer : NSObject
/**
Block方式的定时器
@param task 任务(这里使用block)
@param start 开始时间
@param interval 间隔
@param repeats 时候否重复调用
@param async 同步异步
@return 定时器标识(最终取消定时器是需要根据此标识取消的)
*/
+ (NSString *)timerTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
/**
Target方式的定时器
@param target 目标对象(这里使用方法)
@param selector 调用方法
@param start 开始时间
@param interval 间隔
@param repeats 是否重复调用
@param async 同步异步
@return 定时器标识(最终取消定时器是需要根据此标识取消的)
*/
+ (NSString *)timerTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
/**
取消定时器
@param timerName 定时器标识
*/
+ (void)canelTimer:(NSString *)timerName;
/**
暂停定时器
@param timerName 定时器标识
*/
+ (void)pauseTimer:(NSString *)timerName;
/**
继续定时器
@param timerName 定时器标识
*/
+ (void)continueTimer:(NSString *)timerName;
@end
NS_ASSUME_NONNULL_END
#import "ZXKGCDTimer.h"
@implementation ZXKGCDTimer
//全局字典,存放timer对象
static NSMutableDictionary *timers_;
//信号量,对字典的操作进行加锁操作
dispatch_semaphore_t semaphore_;
+ (void)initialize {
//GCD一次性函数
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)timerTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL) repeats
async:(BOOL)async{
/**
对参数做一些限制
1.如果task不存在,那就没有执行的必要(!task)
2.开始时间必须大于当前时间
3.当需要重复执行时,重复间隔时间必须 >0
以上条件必须满足,定时器才算是比较合理,否则没必要执行
*/
if (!task || start < 0 || (interval <= 0 && repeats)) {
return nil;
}
/**
队列
async:YES 异步 全局队列 dispatch_get_global_queue(0, 0) 可以简单理解为其他线程(非主线程)
async:NO 同步 dispatch_get_main_queue() 可以理解为主线程
*/
dispatch_queue_t queue = async ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) : dispatch_get_main_queue();
/**
创建定时器 dispatch_source_t timer
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 信号量加锁
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *timerName = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[timerName] = timer;
dispatch_semaphore_signal(semaphore_);
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
//定时任务
task();
//如果不需要重复,执行一次即可
if (!repeats) {
[self canelTimer:timerName];
}
});
//启动定时器
dispatch_resume(timer);
return timerName;
}
+ (NSString*)timerTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async{
if (!target || !selector) return nil;
return [self timerTask:^{
if ([target respondsToSelector:selector]) {
//(这是消除警告的处理)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}
+ (void)canelTimer:(NSString *)timerName {
if (timerName.length == 0) {
return;
}
// 信号量加锁
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[timerName];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:timerName];
}
dispatch_semaphore_signal(semaphore_);
}
+ (void)pauseTimer:(NSString *)timerName {
if (timerName.length == 0) {
return;
}
dispatch_source_t timer = timers_[timerName];
if (timer) {
dispatch_suspend(timer);
}
}
+ (void)continueTimer:(NSString *)timerName {
if (timerName.length == 0) {
return;
}
dispatch_source_t timer = timers_[timerName];
if (timer) {
dispatch_resume(timer);
}
}
@end
- 最终使用
包括了block和target两种样式
#import "ZXKGCDTimerVC.h"
@interface ZXKGCDTimerVC ()
@property (nonatomic, strong) NSString *gcdTimer;
@property (nonatomic, assign) int count;
@end
@implementation ZXKGCDTimerVC
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 100;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didBecomeAction)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willResignAction)
name:UIApplicationWillResignActiveNotification
object:nil];
__weak typeof(self) weakSelf = self;
self.gcdTimer = [ZXKGCDTimer timerTask:^{
[weakSelf timerAction];
} start:0 interval:1 repeats:YES async:YES];
// self.gcdTimer = [ZXKGCDTimer timerTask:[ZXKProxy proxyWithTarget:self] selector:@selector(timerAction) start:0 interval:1 repeats:YES async:YES];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[ZXKGCDTimer canelTimer:self.gcdTimer];
}
- (void)timerAction {
NSLog(@"每秒执行--%d", self.count);
self.count--;
}
- (IBAction)pauseAction:(id)sender {
[ZXKGCDTimer pauseTimer:self.gcdTimer];
}
- (IBAction)continueAction:(id)sender {
[ZXKGCDTimer continueTimer:self.gcdTimer];
}
- (void)didBecomeAction {
NSLog(@"进入前台");
}
- (void)willResignAction {
NSLog(@"退到后台");
}
- (void)dealloc {
NSLog(@"%s", __func__);
[ZXKGCDTimer canelTimer:self.gcdTimer];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
运行结果如下图:
image.png
结束语
到这里,GCD定时器相关的内容基本上告一段落,提供了2套GCD的封装,这里建议使用block方式。今后在项目中使用的时候就使用GCD形式的定时器了
网友评论