iOS GCD定时器使用及封装

上篇文章有说到定时器(NSTimer、CADisplayLink),现在补充上篇遗留的GCD的相关东西

  • GCD的简单使用
    这里非常不建议纯手动书写,误差太大,根据代码提示如下所示:


    iOS GCD定时器使用及封装_第1张图片
    图片.png

    选择上图中蓝色选项,结果如下

    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, <#dispatchQueue#>);
    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, ^{
        <#code to be executed when timer fires#>
    });
    dispatch_resume(timer);

最终补充对应的参数等,稍微解释下上面的几个参数


    /*
    这里需要一个队列
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, <#dispatchQueue#>);
    /*
     第一个参数:定时器对象
     第二个参数: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, ^{
        //定时器实现
        <#code to be executed when timer fires#>
    });

接下来看看相对比较完整的写法

    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_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(@"----");
    });
    dispatch_resume(timer);

上面基本上算完成,这里必须使用strong类型接收下,不然定时器没法运行

  • 一般的GCD写法
@interface AnotherViewController ()
@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_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);
}

运行结果如下:


iOS GCD定时器使用及封装_第2张图片
图片.png

GCD的定时器写法相对比较固定,这里建议封装,封装之前先理一理思路(GCD定时器的封装必然依赖原有的写法,所以原有的一些关键性的参数必须保留):
1、开始时间
2、时间间隔
3、是否需要重复执行
4、是否支持多线程
5、具体执行任务的函数或者block,其实这里两种都是可以的
6、取消定时任务
上述就是简单的思路,接下来先进行简单的封装,如下所示:

  • 初步封装
@interface JCGCDTimer : NSObject

+ (void)timerTask:(void(^)(void))task
            start:(NSTimeInterval) start
         interval:(NSTimeInterval) interval
          repeats:(BOOL) repeats
             async:(BOOL)async;

+(void)canelTimer;

@end
@implementation JCGCDTimer

+ (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的形式进行完善上述定时器,具体如下:

  • 进一步封装(这里关于load、initialize的选择抽时间再讲,必要的话专门开一篇细说)
@interface JCGCDTimer : NSObject
+ (NSString*)timerTask:(void(^)(void))task
            start:(NSTimeInterval) start
         interval:(NSTimeInterval) interval
          repeats:(BOOL) repeats
             async:(BOOL)async;
+(void)canelTimer:(NSString*) timerName;

@end
@implementation JCGCDTimer

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

上面的代码涉及到对字典的存取,所以还需要考虑到线程安全(只需要在存值取值的时候进行处理),因此,需要进一步完善代码,如下所示:

  • 最终封装
@interface JCGCDTimer : NSObject

+ (NSString*)timerTask:(void(^)(void))task
            start:(NSTimeInterval) start
         interval:(NSTimeInterval) interval
          repeats:(BOOL) repeats
             async:(BOOL)async;

+(void)canelTimer:(NSString*) timerName;

@end
@implementation JCGCDTimer

static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;

/**
 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);
    
    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;
}

+(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_);

}

@end

上面是以block的方式进行封装,当然了,也可以使用target的方式,相对比较简单,具体如下:

#import 

@interface JCGCDTimer : 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;

@end
#import "JCGCDTimer.h"

@implementation JCGCDTimer

static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;

/**
 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的语法,里面只有一行时候可以省略{},其他的没区别)
    
    /**
     队列
     asyc:YES 全局队列 dispatch_get_global_queue(0, 0) 可以简单理解为其他线程(非主线程)
     asyc: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_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_);

}
@end

最终Controller中的定时器使用

  • block 样式
- (void)viewDidLoad {
    [super viewDidLoad];
    //这里async传值为YES,即在子线程
    NSString* jcdTimer = [JCGCDTimer timerTask:^{
        NSLog(@"%@", [NSThread currentThread]);
    } start:1 interval:2 repeats:YES async:YES];
    
    self.jcTimer = jcdTimer;
}
-(void)dealloc{
    
    NSLog(@"%s", __func__);
    [JCGCDTimer canelTimer:self.jcTimer];
}

运行结果如下:


iOS GCD定时器使用及封装_第3张图片
图片.png
  • target样式
- (void)viewDidLoad {
    [super viewDidLoad];
    //这里async传值为NO,即在主线程
    NSString* jcdTimer = [JCGCDTimer timerTask:self selector:@selector(jcTimerAction) start:1 interval:2 repeats:YES async:NO];
    self.jcTimer = jcdTimer;
}

-(void)jcTimerAction{
    
    NSLog(@"%@", [NSThread currentThread]);
}
-(void)dealloc{
    
    NSLog(@"%s", __func__);
    [JCGCDTimer canelTimer:self.jcTimer];
}

运行如下所示:


iOS GCD定时器使用及封装_第4张图片
图片.png
  • 这里我们发现Controller出栈的时候没有自动调用dealloc,定时器一直没有停止,这里存在循环引用,target/self(这个很容易被忽视),参考iOS 定时器(NSTimer/CADisplayLink)循环引用,这篇文章中JCKProxy(集成自NSProxy),最终修改如下:
    NSString* jcdTimer = [JCGCDTimer timerTask:[JCKProxy proxyWithTarget:self] selector:@selector(jcTimerAction) start:1 interval:2 repeats:YES async:NO];
    self.jcTimer = jcdTimer;

最终运行如下:


iOS GCD定时器使用及封装_第5张图片
图片.png
GCD小结:

1、GCD定时器不依赖NSRunLoop,他是一个独立的体系
2、精度高,最小到1纳秒
3、没有invalidate方法
4、不需要手动管理内存(这里封装的target方式一定要注意循环引用)

结束语

到这里,GCD定时器相关的内容基本上告一段落,提供了2套GCD的封装,这里建议使用block方式,如果其他问题,欢迎私戳,一起进步,共同迈向大神的行列,年轻人,加油 。。。

你可能感兴趣的:(iOS GCD定时器使用及封装)