iOS-多线程(四)-GCD定时器

日常的开发中,我们经常会用到计时器。在 iOS 中,有三种计时器, NSTimerCADisplayLinkdispatch_source ,这三种定时器都是各有优劣。

NSTimer

NSTimer 是使用的比较多的一种,但是精度不够,其原因如下:

  1. NSTimer 加在 main runloop 中,模式是 NSDefaultRunLoopModemain 负责所有主线程事务,例如 UI 界面的操作、复杂的运算等等,这样在同一个 runloopNSTimer 就容易产生阻塞。
  2. 模式的改变也会影响到 NSTimer 的精度。主线程的 runLoop 里有两个预置的 modekCFRunLoopDefaultModeUITrackingRunLoopMode 。当你创建一个 NSTimer 并加到 DefaultMode 时, NSTimer 会得到重复回调,但此时如果滑动一个 scrollView 时, runLoop 会将 mode 切换为 TrackingRunLoopMode ,这时 NSTimer 就不会被回调。所以就会影响到 NSTimer 的精度。
  3. NSTimer 会强引用 target ,而 runLoop 会强持有 NSTimer ,很容易出现内存泄漏。

那么如何让 NSTimer 精准一些?

  1. NSTimer 实例加到 main runloop 的特定 mode 中,避免被复杂运算操作或者 UI 界面刷新所影响。
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(showTime) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
复制代码
  1. 在子线程中进行 NSTimer 的操作,然后在主线程中修改 UI 界面显示操作结果。
- (void)timerMethod {
     NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
     [thread start];
}
- (void)newThread {
     @autoreleasepool
     {
          [NSTimer scheduledTimerWithTimeInterval:1.0 target:self           selector:@selector(showTime) userInfo:nil repeats:YES];
          [[NSRunLoop currentRunLoop] run];
     }
}
复制代码

CADisplayLink

CADisplayLink 是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。 CADisplayLink 以特定模式注册到 runloop 后, 每当屏幕显示内容刷新结束的时候, runloop 就会向 CADisplayLink 指定的 target 发送一次消息, CADisplayLink 类对应的 selector 就会被调用一次。

iOS 设备的屏幕刷新频率是固定的, CADisplayLink 在正常情况下会在每次刷新结束都被调用,精确度相当高。 CADisplayLink 使用场合相对专一,一般用于做 UI 界面的不停重绘,比如自定义动画引擎或者视频播放的渲染。

使用方式如下:

- (void)startDisplayLink {
     self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
     [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)handleDisplayLink:(CADisplayLink *)link {

}

- (void)stopDisplayLink {
  [self.displayLink invalidate];
  self.displayLink = nil;
}
复制代码

dispatch_source

dispatch_source_t 的定时器不受 RunLoop 影响,而且 dispatch_source_t 是系统级别的源事件,精度很高,系统自动触发。

下面我们就来定一个基于 dispatch_source_t 的定时器。

首先,我们知道 dispatch_source_t 源事件有一种类型就是 DISPATCH_SOURCE_TYPE_TIMER ,用来计时的。 dispatch_source_t 的步骤如下:

    1. 创建源事件
    1. 设置定时器事件
    1. 设置事件触发的回调
    1. 运行

另外我们还需要在合适的地方设置源事件的数据。

其次,我们知道,定义一个定时器,就需要运行、暂停、销毁的功能。以及设置定时器定时的时间、是否重复、回调等配置。

另外,我们要对外暴露实例方法、类方法来方便外界使用。

根据以上准备工作,我们创建一个 TGCDTimer 类。

@interface TGCDTimer : NSObject

/// 默认主线程创建
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval repeat:(BOOL)repeat block:(dispatch_block_t)block;
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval repeat:(BOOL)repeat queue:(dispatch_queue_t)queue block:(dispatch_block_t)block;
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeat:(BOOL)repeat queue:(dispatch_queue_t)queue block:(dispatch_block_t)block;
/// 默认主线程创建
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeat:(BOOL)repeat block:(dispatch_block_t)block;
- (void)setTimeInterval:(NSTimeInterval)interval;
- (void)stop;
- (void)restart;
- (void)invalidate;

@end
复制代码

然后我们来实现方法:

@interface TGCDTimer() {
    dispatch_source_t _timer;
    BOOL _isFire;
}

@end

- (instancetype)initWithTimeInterval:(NSTimeInterval)interval repeat:(BOOL)repeat queue:(dispatch_queue_t)queue block:(dispatch_block_t)block {
    // 1. 确定传进来的队列是必须存在的
    NSAssert(queue != NULL, @"queue can't be NULL while create TGCDTimer");
    
    if (self = [super init]) {
        // 2. 创建定时器
        _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        // 3. 设置定时器的时间
        dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval * NSEC_PER_SEC, 0);
       
        // 4. 监听定时器的回调
        dispatch_source_set_event_handler(_timer, ^{
            // 将定时器的回调,传给外界
            if (block) {
                block();
            }
            // 如果不重复,执行一次之后就销毁定时器
            if (!repeat) {
                self->_isFire = NO;
                dispatch_source_cancel(self->_timer);
            }
        });
        
        // 5. 在给定的interval之后启动定时器
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            dispatch_resume(self->_timer);
            self->_isFire = YES;
        });
    }
    
    return self;
}
复制代码

实现定时器的重启、暂停、销毁等方法:

- (void)stop {
    if (_isFire) {
        _isFire = NO;
        dispatch_suspend(_timer);
    }
}

- (void)restart {
    if (!_isFire) {
        _isFire = YES;
        dispatch_resume(_timer);
    }
}

- (void)invalidate {
    _isFire = NO;
    dispatch_source_cancel(_timer);
}

- (void)dealloc {
    _isFire = NO;
    dispatch_source_cancel(_timer);
}
复制代码

这样一个简单的 GCD 定时器就实现了。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章