美文网首页IOS将来跳槽用IOS
定时器集合 NSTimer & CADisplayLin

定时器集合 NSTimer & CADisplayLin

作者: CoderHG | 来源:发表于2018-04-01 19:23 被阅读530次

    零、说在前面的

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


    项目简单整理

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

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

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

    一、定时器集合

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

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

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

    1.1 NSDelayedPerforming

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


    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 & CADisplayLin

      本文链接:https://www.haomeiwen.com/subject/rlvocftx.html