选择 GCD 还是 NSTimer ?

作者: Joey_Xu | 来源:发表于2015-06-17 22:32 被阅读18256次

    我们常常会延迟某件任务的执行,或者让某件任务周期性的执行。然后也会在某些时候需要取消掉之前延迟执行的任务。

    延迟操作的方案一般有三种:

    1.NSObject的方法:

    gcdTimer

    2.使用NSTimer的方法:

    gcdTimer

    3.使用GCD的方法:

    gcdTimer

    一般情况下,我们选择使用GCD的dispatch_after。

    因为如果不用GCD,编码需要注意以下三个细节:

    1.必须保证有一个活跃的runloop。

    performSelector和scheduledTimerWithTimeInterval方法都是基于runloop的。我们知道,当一个应用启动时,系统会开启一个主线程,并且把主线程的runloop激活,也就是run起来,并且主线程的runloop是不会停止的。所以,当这两个方法在主线程可以被正常调用。但情况往往不是这样的。实际编码中,我们更多的逻辑是放在子线程中执行的。而子线程的runloop是默认关闭的。这时如果不手动激活runloop,performSelector和scheduledTimerWithTimeInterval的调用将是无效的。

    2.NSTimer的创建与撤销必须在同一个线程操作、performSelector的创建与撤销必须在同一个线程操作。

    3.内存管理有潜在泄露的风险

    scheduledTimerWithTimeInterval方法将target设为A对象时,A对象会被这个timer所持有,也就是会被retain一次,timer会被当前的runloop所持有。performSelector:withObject:afterDelay:方法实际上是在当前线程的runloop里帮你创建的一个timer去执行任务,所以和scheduledTimerWithTimeInterval方法一样会retain其调用对象。但是,我们往往不希望因为这些延迟操作而影响对象的生命周期,更甚至是,导致对象无法释放。举个例子:

    gcdTimer

    你创建的对象X中有一个实例变量_timer,方法fireTimer触发一个timer,方法cancel取消这个timer。在对象X需要销毁的时候,需要将它的timer取消掉。

    gcdTimer

    不幸的是,dealloc方法将永远不会被调用。因为timer的引用,对象X的引用计数永远不会降到0,dealloc方法也就不会被调用。这时如果不调用cancel,对象X将永远无法释放,造成内存泄露。想想一个对象若不调用某一个方法就会造成内存泄露,这该是多大一个坑。

    This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.

    If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

    You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.

    上面摘自苹果官方文档对invalidate方法的解释。可以看到,当一个timer被schedule的时候,timer会持有target对象,NSRunLoop对象会持有timer。当invalidate被调用时,NSRunLoop对象会释放对timer的持有,timer会释放对target的持有。除此之外,我们没有途径可以释放timer对target的持有。所以解决内存泄露就必须撤销timer,若不撤销,target对象将永远无法释放。

    若使用dispatch_after,系统会帮我们处理线程级的逻辑,这样也我们更易于享受系统对线程所做的优化。除此之外,我们不用关心runloop的问题。并且调用的对象也不会被强行持有,这样上述的内存问题也不复存在。当然,需要注意block会持有其传入的对象,但这可以通过weakself解决。所以在这种延迟操作方案中,使用dispatch_after更佳。

    但是呢,dispatch_after有个致命的弱点:dispatch_after一旦执行后,就不能撤销了。而performSelector可以使用cancelPreviousPerformRequestsWithTarget方法撤销,NSTimer也可以调用invalidate进行撤销。(注意:撤销任务与创建timer任务必须在同一个线程,即同一个runloop)所以我们还是得用NSTimer或者performSelector吗?

    NO,其实GCD也有timer的功能。用GCD来实现一个timer:

    gcdTimer

    这样我们就规避了NSTimer的三个缺陷。

    到这里问题基本得到了解决,但是我们还可以做的更好:)

    1.GCD的timer使用的API比较冗余,每次使用都会copy代码。2.没有repeats的选项,若只想执行一次还得自己写标记位控制。这些问题我们都可以封装成一个统一的API:

    gcdTimer

    这样,外部只需调用这个两个接口,用起来和NSTimer一样方便!

    gcdTimer

    上面的代码就创建了一个名叫myTimer的timer,这个timer将在2 seconds后执行一个block,随后timer自动停止并被释放。当然,如果repeats参数传入的是YES,那么这么timer会一个周期接一个周期的执行,直到你cancel掉这个timer。

    当然,你可以在self对象的dealloc方法里面做cancel,这样保证了timer恰好运行于整个对象的生命周期中。这是NSTimer和performSelector所做不到的事情。你也可以通过queue参数控制这个timer所添加到的线程,也就是action最终执行的线程。传入nil则会默认放到子线程中执行。UI相关的操作需要传入dispatch_get_main_queue()以放到主线程中执行。

    Well, we can actually do even better.

    注意到,我们经常遇到的场景是,在开始新一次计时的时候,取消掉上一次的计时。也就是每次schedule之前先cancel。这部分对任务处理的能力,也是可以集成到我们的组件中的。我们可以向外部提供一个枚举类型的选项,以选择其对任务的处理类型:

    gcdTimer

    或者场景是,在开始新一次计时的时候,取消上一次的计时,但是将上一次计时的任务,合并到新的一次计时中,最终一并执行。

    针对这两种场景,JX_GCDTimerManager提供了两个option选项:

    gcdTimer

    如果你不care这些使用场景的话,默认使用AbandonPreviousAction就行了。需要注意的是,同一个timer建议保持同一个任务处理方式,即相同的ActionOption,如果需要切换option,请注意一下切换的衔接问题。

    大家也可以自行去对actionOption做扩展,以满足常见的使用场景。

    详细的实现,见git源码。

    综上,选择使用GCD的技术有助于我们提高代码的健壮性与稳定性。

    注:文中若有错误,恭请斧正哈!:)

    源码地址:Joeyqiushi/JX_GCDTimer · GitHub

    相关文章

      网友评论

      • 瞎编程:楼主你好,repeats:NO的时候是不是不需要手动cancel
        Joey_Xu:@瞎编程 是的,不需要手动cancel
      • 我本善良:gcd创建之后,调用resum会立刻调用。比如我创建一个gcdtimer,想让它在30s之后调用,而不是立刻调用,怎么办
        Joey_Xu:@我本善良 设置30秒的interval,第一次执行timer就是30秒之后。
      • LoyalToOrigin:当然,需要注意block会持有其传入的对象,但这可以通过weakself解决。
        GCD的block不会持有self吧!
      • 天地逍遥:楼主,能不能加一个取消timer的回掉事件
        Joey_Xu:@天地逍遥 这个自己可以随意添加啦,比较简单,这个诉求比较特殊
      • 张三石:将同一个timer之前的任务合并到新的任务中是什么意思,不太明白啊
        Joey_Xu:@张三石 规划myTimer2秒后执行任务A,跑到1.5秒的时候,又规划myTimer2秒后执行任务B,这时可以让任务A和B同时在3.5秒的时候执行。不明白的话,忽略也可以,这个策略我也没用过:grin:
      • 东野浪子:看了下楼主的代码,分装的真心不错。不过有个小疑问,那个ActionOption真的有合并的这种需求吗?如果有类似需求的话为何不新建一个timer呢?
        Joey_Xu:@东野浪子 目前我还没遇到过合并的需求,所以我的项目中直接就去掉这个ActionOption参数了:smile:
      • 南方小金豆:- (void)cancelTimerWithName:(NSString *)timerName {
        dispatch_source_t timer = [self.timerContainer objectForKey:timerName];
        if (!timer) {
        return;
        }
        [self.timerContainer removeObjectForKey:timerName];
        dispatch_source_cancel(timer);
        [self.actionBlockCache removeObjectForKey:timerName];
        }
        楼主,最近遇到cancelTimerWithName:的时候崩溃了。 找不到原因
        Joey_Xu:@那份牵挂给了谁 有堆栈吗?QQ413326183,发给我看看呢
      • leejunhui:使用GCD来代替NSTimer,效果立竿见影!
      • H_Cynic:不幸的是,dealloc方法将永远不会被调用。因为timer的引用,对象X的引用计数永远不会降到0,dealloc方法也就不会被调用。这时如果不调用cancel,对象X将永远无法释放,造成内存泄露。想想一个对象若不调用某一个方法就会造成内存泄露,这该是多大一个坑。---说的是NSTimer
        GCD一样有这个问题啊 如果我使用GCD倒计时60秒 时间为0取消计时器 dispatch_cancel(timer1);但是如果我在中途退出页面 dealloc一样不会走 还是一直在倒计时 这说明控制器引用计数不为0 只能像处理NSTimer一样在页面消失的方法里(viewDidDisappear)销毁掉 GCD调用dispatch_source_cancel(timer1);然后才会dealloc
        这样看来是不是可以说GCD和NSTimer的区别就在于是否会延迟执行和精确度上面了?
        :grin: 个人看法 请批评指正。。。
        H_Cynic:@Joey_Xu 多谢指教:pray:
        Joey_Xu:@H_Cynic GCD使用的block可以用weakSelf,这样就可以在dealloc里面cancel掉GCD的timer了。
      • iOS_Coder_: dispatch_source_set_event_handler(timer, ^{
        NSMutableArray *actionArray = [self.actionBlockCache objectForKey:timerName];
        [actionArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        dispatch_block_t actionBlock = obj;
        actionBlock();
        }];
        // [weakSelf removeActionCacheForTimer:timerName];

        if (!repeats) {
        [weakSelf cancelTimerWithName:timerName];
        }
        });

        这里貌似多了一行代码,导致不能重复调用block
      • JerseyBro:干货
      • AKsoftware:2.NSTimer的创建与撤销必须在同一个线程操作,我尝试结果并不是?能否给一个解释
        洛奇丶:你们有把NSTimer添加到runloop里面再试么
        啊哈呵:这个问题我测试,也能取消啊。不知道博主是怎么样子代码测试?
      • lxyz22zp:swift中 dispatch_after 相对于OC好用不少

        func delay(time: NSTimeInterval, work: dispatch_block_t) -> CancelableTask? {
        var finalTask: CancelableTask?
        let cancelableTask: CancelableTask = { cancel in
        if cancel {
        finalTask = nil // key
        } else {
        dispatch_async(dispatch_get_main_queue(), work)
        }
        }
        finalTask = cancelableTask
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(time * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
        if let task = finalTask {
        task(cancel: false)
        }
        }
        return finalTask
        }

        func cancel(cancelableTask: CancelableTask?) {
        cancelableTask?(cancel: true)
        }
      • Delpan:还有一个区别,NSTimer可以使用在你自己创建的子线程里用,而GCD定时器是做不到的
        Delpan:@王银博 GCD定时器是以队列为目标来工作的,并不能在你所创建的NSThread线程中用
        charlotte2018:@Delpan 放到多线程可以执行啊
        9ff96de9a34e:好像可以的吧,你自己写代码验证了没
      • 上善若水Ryder:值得研究学习,真强
      • 4aec6b0a15de:写得太好了,感谢博主。
        有个问题想请教下博主:
        gcd 的timer 和runloop timer 在原理上有什么不同, runloop 的timeout 就是用gcd 的dispatch source 实现的。对于其他加在 runloop 上的timer 有什么区别?
      • 原鸣清:博主你好,在github上看了您的代码,代码很好,值得学习,在看的时候隐约发现一个问题,在 case MergePreviousAction: 的实践中您先cach了本次action,随后又remove了cache,这样在actionBlockCache中不是又空了么?是我没读懂么?希望能得到您的回复
        Joey_Xu:@原鸣清 业务逻辑,不用纠结怎么做cache的哈。明白timer的坑,然后根据你的具体应用场景去做相应的行为就好。
        Joey_Xu:@snow_leopard_ck 执行完成后,删除本次任务,即从cache中删除。
      • 原鸣清:文章写的棒棒哒,收了,补充个@fuadam1982 兄台提出的关于“在xcode 7下,gcd timer的执行随着调用函数的退出而结束。”的问题。研究下了这句话说的,我觉得应该是在新的SDK中,苹果对于gcd_timer做的优化,就是它活干完了,就主动把它cancel了。倒不是我之前说的全局不全局的问题,问题在于coder有没有主动的cancel的动作,我的理解是,当你的code主动call了Dispatch_cancel的动作,它就不会去主动关你的gcd_timer;我是尝试将block的部分写成
        if (0) {
        dispatch_source_cancel(timer);
        }
        后得出的结论,请大神们参与讨论,助小弟解惑。
      • 2d57f10c5da5:博主
        一个tableview里面多个cell都用到这个倒计时 应该怎么操作?
        Joey_Xu:@2d57f10c5da5 你说的场景我不太明白呢,是不是每个重用cell都加一个timer?
      • fuadam1982:在xcode 7下,gcd timer的执行随着调用函数的退出而结束。
        starfox寒流:@snow_leopard_ck 若是方法中定义个timer,在方法结束之后就会被释放了,所以就不执行了.需要把timer定义为全局变量。
        starfox寒流:@snow_leopard_ck 应该是说gcd timer的创建是在某一个方法中的,当这个方法结束之后 ,gcd timer也就不执行了。
        原鸣清:@fuadam1982 你好,我发现如果是全局的gcd timer 是可以达到我想要timer的效果,但是局部变量的貌似不行,我也不敢确定,前几天写的一个可以,今天写的,不行,搞了半天也没搞清楚为什么。能回复下关于你说的“gcd timer的执行随着调用函数的退出而结束”是什么意思吗,详细些的。 :smile:
      • 6fdb0c58ceca:长见识了,GCD的Timer一直用的不熟,学习学习
      • ddaa8dae50b0:干货~赞之

      本文标题:选择 GCD 还是 NSTimer ?

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