定时器集合 NSTimer & CADisplayLink & dispatch_source_t & dispatch_after & NSDelayedPerforming

零、说在前面的

最近趁着悠闲,所以总是想写点什么,主要是为了总结。不总结、恐怕以后就被遗忘了,总结一下、也能很好的巩固一下。
在介绍主题之前,先来看看下面的这张图片:


定时器集合 NSTimer & CADisplayLink & dispatch_source_t & dispatch_after & NSDelayedPerforming_第1张图片
项目简单整理

这张图片,没有什么,就是一个目录的简单整理。我在iOS项目的搭建到分发的介绍中有一个实际的项目,有几个简简单单的小功能与三个小 pod 库之外也没有什么,感兴趣的话可以去看看。

在即将介绍的 时间集合 过程中,也会有一个简单的项目。记得下载。本介绍仅仅是对所有的定时器的简单时间而已,看似简单,里面可能有你未曾注意的地方。本文介绍的都是一些细枝末节的技术点,往往会在面试的过程中起到关键性的作用。

代码在这里HGTimeSet、欢迎下载。

一、定时器集合

强调一下:本篇介绍仅仅是介绍如何的去使用各种的定时器、以及避免错误的使用方式而已,对于详细的底层原理,我没有打算要介绍。毕竟定时器的底层原理是与 Runloop 有关的,那是一个很大的话题。

大概先在这里列举一下接下来要介绍的在 iOS 开发中可能会用到的定时器:

  • 1、系统分类(NSDelayedPerforming)方法
  • 2、线程派发 dispatch_after
  • 3、NSTimer
  • 4、dispatch_source_t
  • 5、CADisplayLink

1.1 NSDelayedPerforming

点击进去,会看到如下几个方法,前两个就是定时器方法:


定时器集合 NSTimer & CADisplayLink & dispatch_source_t & dispatch_after & NSDelayedPerforming_第2张图片
NSDelayedPerforming 分类方法

一个简单的例子:

NSLog(@"123");

// 三秒过后再执行
[self performSelector:@selector(testSelectorDelay) withObject:nil afterDelay:3];
NSLog(@"321");

测试方法:

// 一个简单的测试方法
- (void)testSelectorDelay {
    NSLog(@"testSelectorDelay");
}

打印顺序是这样的: 123、321、testSelectorDelay。说明这么使用不会阻塞当前的线程、在使用上也不会出现什么问题,正常使用即可。

1.2 线程派发 dispatch_after

代码如下:

NSLog(@"123");
    
    // 3秒钟之后执行 block
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"定时器触发");
    });
    
    NSLog(@"321");

运行代码, 打印顺序是:123321定时器触发。也说明这么使用不会阻塞当前的线程、在使用上也不会出现什么问题,正常使用即可。

综上的两个定时器,一般只要是正常使用, 就没有什么问题。但是有一个问题,一旦使用,根本就没有方法暂停定时器。这是一个痛点。

1.3 NSTimer

这个定时器是大家再熟悉不过的了。这个定时器,有两种使用方式:Block 与 Target。接下来看看都是如何使用之。
在这里想插一嘴的是,关于这个定时器的 类创建 方法是分为两大类的,分别是以 timerscheduled的,具体有什么区别,如果你不知道的话、那就是很少使用,或者就是每次使用都是在 copy 别人的代码,那么现在可以去查看相关文档以及亲自试验一下。
除了以上特点,还有一个是即将要介绍的特点:支持 Block 与 Target 两种使用方式,接下来分别介绍一下。

在接下来的所有介绍中, 每个定时器的 repeats 的值都是 YES。

1.3.1 Timer Block

第一种使用方式

具体使用,如下:

    // 开启定时 repeats 的值是 YES
    [NSTimer scheduledTimerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"定时器的 Block 被执行");
    }];

很简单、也正常。但是有一个问题:这个定时器根本停不下来。
处理方式,很简单。弄一个 NSTimer 的属性来与之关联,就能在指定的时机停止当前的定时功能。

定义一个这个的 属性:

// 现在的内存语义是 strong
@property (nonatomic, strong) NSTimer* blockTimer;

比如,我们希望在当前使用对象销毁的时候,停止定时器,那么我们可以这么做:

- (void)dealloc {
#if DEBUG
    NSLog(@"你的离开,是我唯一的期待");
#endif
    
    // 停止定时器
    {
        // 尽量不要使用 self.blockTimer
        [_blockTimer invalidate];
        _blockTimer = nil;
    }
}

运行代码,发现现在定时器能正常的在当前对象中使用时,能在销毁的时候停止了。

上面有两个地方的注释,感觉怪怪的,值得注意一下:

  • 1、// 尽量不要使用 self.blockTimer 几乎所以的大神都建议,在 dealloc 方法中,尽量不要使用 . 语法对成员变量的访问。会容易触发 KVO 以及其它额外的操作。当然了,这是一个习惯的问题,如果这个属性根本就没有做任何的 KVO 以及实现什么 getter 与 setter 方法的话,也没有什么影响。但是有一个问题是,当前的 class 没有做,不代表以后的子类不做。所以尽量不要使用 . 语法。
  • 2、// 现在的内存语义是 strong 关于这个注释,看下面的介绍

在上面的例子中,其实 blockTimer 完全没有必要弄成一个属性的,简单的定义一个成员变量即可。但是我这么弄,是有目的的。接下来,我们把这里的内存语义由 strong 变成 weak,看看会有什么效果。
想想刚刚我们为了出来定时器根本停不下来的时候, 才定义这么一个属性, 现在将这个内存语义变成了 weak,这个时候回想到我们是怎么给这个属性赋值的:

// 开启定时 repeats 的值是 YES
self.blockTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"定时器的 Block 被执行");
}];

是的,直接将创建好的一个定时器给了一个 weak 类型的指针。这样,不可以吧?!运行代码看看效果。
运行代码之后发现,功能完全没有受到任何的 威胁。仔细的研究之后,发现主要的原因_blockTimer 这个成员变量一直都是有值的。但是这个属性是 weak 指针呐,那么又说明另一个问题,在 另一个地方 还有一个 类似 强指针的指针指向了这个定时器的内存(这里我只敢说是类似因为我还没有看过具体的源码,暂时是猜的)。
好像这个 strong 与 weak 对我们的影响不是太大,只要我们最后能调用 invalidate 以及设置成 nil 就没有其它的什么问题。是的,确实是这样,但是我也不是随便这么一说的,我说出来,还是有点道理的,怕你在接下来被我所说的产生更大的误解。

接下来,我想让你忘记 blockTimer 这个属性、假装我从来没有定义过。但是我的代码,想改成这个样子的:

// 开启定时 repeats 的值是 YES 没有 blockTimer
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"定时器的 Block 被执行, %@", self);
}];

一看这代码就与刚开始的代码一样啊,再仔细一看仅仅是在 block 使用了一下 self 而已。刚开始的时候,发生的 事故 是定时器根本就停不下来。这一次又会发生点什么呢?运行代码,就会发现这次不仅是定时器根本就停不下来这么简单了,就连当前的实例对象也无法释放了。换言之,内存泄漏了。我左看下看,也没有看到哪里出现了指针循环啊。带着无限的恐惧,修改成这样:

// 将 self 弱化
__weak typeof(self) weakSelf = self;
// 开启定时 repeats 的值是 YES 没有 blockTimer
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"定时器的 Block 被执行, %@", weakSelf);
}];

运行代码,有新的结论了:当前的实例对象 self 能正常的销毁了,但是呢问题有回到了定时器根本停不下来的状态 block 中的 weakSelf 一直为空。那这说明了什么呢?说明上面的那一个结论:在 另一个地方 还有一个 类似 强指针的指针指向了这个定时器的内存。所谓的 另一个地方 应该就是与这个 self 是有关的,有 类似强指针 的 指针 关系的。
上面的每种使用都有问题,那么应该如何才算是正确的使用呢?经过上述介绍,就是最后一种方式配合 blockTimer 来使用即可,最终应该这样使用:

// 将 self 弱化
__weak typeof(self) weakSelf = self;
// 开启定时 repeats 的值是 YES 没有 blockTimer
self.blockTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
   NSLog(@"定时器的 Block 被执行, %@", weakSelf);
}];

完美的解决了如下的问题:

  • 1、定时器根本停不下来
  • 2、强指针导致内存泄漏

关于 blockTimer 是 weak 还是 strong,主要看心情吧。暂时还没有发现其它的什么问题。

上面对 Block 已经通过两个问题,介绍了一堆的东西。在接下来的 Target 中,如果与上面有重复的、类似的,我就不提了。

1.3.2 Timer Target

同样,先弄一个简单的代码:

// 开启定时 repeats 的值是 YES
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTimer) userInfo:nil repeats:YES];

执行方法:

// 测试方法
- (void)testTimer {
    NSLog(@"testTimer");
}

运行代码,发现两个问题:

  • 1、根本停不下来
  • 2、当前实例对象根本不能销毁

厉害了,好像更严重了。如何解决 根本停不下来 的问题呢?与上面 Block 方式的解决方法一样?那肯定不行的,因为这个当前的实例对象,根本就没有销毁,以至于根本就不会调用 -dealloc 的方法。

那么就只能在希望当前实例对象即将需要销毁的时候关闭定时器,但是这个也是不可能的,因为没有提供这样的方法。那就使用代理吧。厉害了,这个代理不是 delegate,而是 NSProxy。这个代理,在很久之前见到过,只可惜没有用过,更别说研究过,更别说面试官问的时候能回答得上来。[有种相见恨晚的感觉]
直接上代码:
定义一个代理:


/**
 一个代理定时器
 */
@interface HGProxy :  NSProxy

@property (nonatomic, weak) id target;

@end

@implementation HGProxy

- (void)proxyAction {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    //
    [_target performSelector:@selector(timerAction)];
    
#pragma clang diagnostic pop
    
}
@end

具体的使用方法:

// 代理  注意:没有 init 方法
HGProxy* proxy = [HGProxy alloc];
// 一定要设置这个值
proxy.target = self;
// 开启定时 repeats 的值是 YES
[NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(proxyAction) userInfo:nil repeats:YES];

上面的代码, 好好的看注释。运行代码,完美的解决问题。但是,太繁琐了,使用起来特别的别扭,感觉方法调来调去的。这个方法是没有问题,但是使用很别扭,还有更好的方案么?答案肯定是 :有的。预知详细内容,请见下节分享。

1.3.3 YYKit 中对 NSTimer 的处理

很多的面试官,就完全可以使用这个问题来反映出面试者是否看过 YYKit 这份优秀的代码。在 YYKit 中有两个文件做了处理。
关于 Target 的其它优化方案,在 YYKit 中 还有两种处理方案,特别的棒。就是这两种:
主要是这个两个文件:NSTimer+YYAdd 与 YYWeakProxy,在我的 demo 中也有实现。

1.4 dispatch_source_t

这也是一个很常见的定时器,只是在使用起来有那么一点的复杂,但是复杂归复杂,功能还是比上面的多的。比如,能通过 block 监听定时器的取消事件。更多的代码,请见 demo,这里将一小段代码展示如下:

// 定时执行的 block
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"定时执行的 block 被执行");
});

// 取消定时器时执行的 block 被dispatch_source_cancel触发
dispatch_source_set_cancel_handler(_timer, ^{
    NSLog(@"取消定时器时执行的 block 被执行");
});

这个定时器与 NSTimer 定时器有所不同,不需要在 -dealloc 中特意的关闭定时器,这里的关闭主要是指 取消。上面的代码是没有问题的,一但当前的实例对象呗销毁了,这个定时器自动就停止了。但是如果需要在取消的时候,去做一些收尾工作的话,那就需要调用一下这个行数了 dispatch_source_cancel 。所以一般情况还是需要在 -dealloc 中关闭一下比较好。

那这个定时器,在使用的过程中需要注意点什么呢?看下面的代码:

// 定时执行的 block
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"定时执行的 block 被执行 %@", self);
});

// 取消定时器时执行的 block 被dispatch_source_cancel触发
dispatch_source_set_cancel_handler(_timer, ^{
    NSLog(@"取消定时器时执行的 block 被执行 %@", self);
});

是的、我有开始搞事情了。指针循环了,看了半天没有循环啊,但是确实是循环了。-dealloc 根本不是被调用。

所以,这里需要注意的是,在以上的两个 block 中,一定要对 self 弱化。这样的,就没有事了:

// weak self
__weak typeof(self) weakSelf = self;
// 定时执行的 block
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"定时执行的 block 被执行 %@", weakSelf);
});

// 取消定时器时执行的 block 被dispatch_source_cancel触发
dispatch_source_set_cancel_handler(_timer, ^{
    // 如果是在 -dealloc 中被取消的话, weakSlef 是没有值的.根据这个特点,可以判断是否是在 -dealloc 中被取消的
    NSLog(@"取消定时器时执行的 block 被执行 %@", weakSelf);
});

OK 了,相对来说,这种定时器的处理方式还算可以,不是那么的复杂。只是这个指针循环隐藏得有点不容易被发现。还有一个特殊的地方就是这个定时器可以通过 block 监听到取消事件。

1.5 CADisplayLink

这种定时器的特点就是,频率与屏幕的刷新频率一致。具体的使用,代码如下:

开启一个定时器
[CADisplayLink displayLinkWithTarget:self selector:@selector(testDisplayLink)];

测试方法是这样的:

// 测试方法
- (void)testDisplayLink {
    NSLog(@"testDisplayLink");
}

运行代码,发现不能运行,正常的姿势是需要将这个定时器加入到当前 Runloop 的模式下,这样的:

CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(testDisplayLink)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

这样就能正常的运行起来了。但是最终发现,出现了像 NSTimer 的 Target 一模一样的问题,解决方案也是一抹一样的,这里不赘述了。

二、总结

综上所述,关于定时器的使用,无外乎就是要注意在使用定时器的过程中所带来的内存泄漏的问题。如果不是亲自的实现,是很难发现的。这主要的原因还是因为在内部做了一下强引用的关联,然后没有被暴露出来,导致很难被发现。 上面的介绍中也一一的给出了,不同的解决方案。
在使用方面,不同的定时器也有着不一样的用法。大致就是:

  • 1、系统分类(NSDelayedPerforming)与线程派发(dispatch_after)这两中是不可控的,无法将其关闭,同时呢也是不是重复的。
  • 2、dispatch_source_t 定时器的特点就是 你能监听定时器被取消。往往用在当定时器被取消之后立马要处理一些处理的时候,显得特别的方便。
  • 3、NSTimer 与CADisplayLink,同病相怜,有很多的相似之处,在用法上也有所不同。第一个就是频率的不同,还有一个是:CADisplayLink被默认加入在 Runloop 中,需要手动添加。当然,要说明一下的是,所有的定时器都是与 Runloop 有关的,可以查看相关的资料。

如果由于刚刚时间仓促,忘记了下拉代码,那么可以直接点击这里点击这里点击这里。

要是有什么不对的、或者需要补充的,感谢评论讨论!

我的更多文章,可以直接看这里NewStart NewStart NewStart

谢谢大家!

你可能感兴趣的:(定时器集合 NSTimer & CADisplayLink & dispatch_source_t & dispatch_after & NSDelayedPerforming)