使用 GCD 运行重复 NSTimer?

Posted

技术标签:

【中文标题】使用 GCD 运行重复 NSTimer?【英文标题】:Run repeating NSTimer with GCD? 【发布时间】:2012-05-18 08:55:41 【问题描述】:

我想知道为什么当您在 GCD 块中创建重复计时器时它不起作用?

这很好用:

-(void)viewDidLoad
    [super viewDidLoad];
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];

-(void)runTimer
    NSLog(@"hi");

但这确实有效:

dispatch_queue_t myQueue;

-(void)viewDidLoad
    [super viewDidLoad];

    myQueue = dispatch_queue_create("someDescription", NULL);
    dispatch_async(myQueue, ^
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
    );

-(void)runTimer
    NSLog(@"hi");

【问题讨论】:

【参考方案1】:

NSTimer 被调度到线程的 runloop。在问题代码中,GCD 调度的线程的运行循环没有运行。您必须手动启动它,并且必须有退出运行循环的方法,因此您应该保留对 NSTimer 的引用,并在适当的时候使其无效。

NSTimer对target有强引用,所以target不能对timer有强引用,runloop对timer有强引用。

weak var weakTimer: Timer?
func configurateTimerInBackgroundThread()
    DispatchQueue.global().async 
        // Pause program execution in Xcode, you will find thread with this name
        Thread.current.name = "BackgroundThreadWithTimer"
        // This timer is scheduled to current run loop
        self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
        // Start current runloop manually, otherwise NSTimer won't fire.
        RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
    


@objc func runTimer()
    NSLog("Timer is running in mainThread: \(Thread.isMainThread)")

如果以后定时器失效了,在Xcode中再次暂停程序执行,你会发现那个线程已经没有了。

当然,GCD 调度的线程有runloop。 GCD 在内部生成和重用线程,线程对调用者是匿名的。如果你觉得不安全,你可以使用 Thread.不要害怕,代码很容易。

实际上,我上周尝试了同样的事情并与提问者同样失败,然后我找到了这个页面。在我放弃之前,我尝试了 NSThread。有用。那么为什么 GCD 中的 NSTimer 不能工作呢?它应该是。阅读runloop's document 了解 NSTimer 的工作原理。

使用 NSThread 与 NSTimer 一起工作:

func configurateTimerInBackgroundThread()
    let thread = Thread.init(target: self, selector: #selector(addTimerInBackground), object: nil)
    thread.name = "BackgroundThreadWithTimer"
    thread.start()


@objc func addTimerInBackground() 
    self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
    RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)

【讨论】:

【参考方案2】:

这是一个坏主意。我正要删除这个答案,但我把它留在这里是为了避免其他人犯同样的错误。 感谢#Kevin_Ballard 指出这一点。

您只需在示例中添加一行,它就会像您编写的那样工作:

[[NSRunLoop currentRunLoop] run]

所以你会得到:

    -(void)viewDidLoad
        [super viewDidLoad];

        myQueue = dispatch_queue_create("someDescription", NULL);
        dispatch_async(myQueue, ^
            [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] run]
        );
    

由于您的队列 @9​​87654323@ 包含一个 NSThread 并且它包含一个 NSRunLoop 并且由于 dispatch_async 中的代码在该 NSThread 的上下文中运行,currentRunLoop 将返回与您的线程相关联的停止运行循环排队。

【讨论】:

这是个坏主意。您现在已经选择了一个调度线程来用作运行循环,并且您永远不会将线程的控制权返回给 GCD。这基本上就像将无限循环放入调度块中。接管调度线程并且从不将它们的控制权返回给 GCD 会损害整个系统的性能,如果你对足够多的线程这样做,你实际上可以让 GCD 完全停止处理提交到非全局队列的块。 你的代码只需要一种退出runloop的方法。保留对 NSTimer 的引用,并在适当的时候使其失效。【参考方案3】:

NSTimers 被安排在当前线程的run loop 上。但是,GCD 调度线程没有运行循环,因此在 GCD 块中调度计时器不会做任何事情。

有三种合理的选择:

    找出您想在哪个运行循环上安排计时器,并明确地这样做。使用+[NSTimer timerWithTimeInterval:target:selector:userInfo:repeats:] 创建计时器,然后使用-[NSRunLoop addTimer:forMode:] 在您要使用的运行循环上实际安排它。这需要处理相关的运行循环,但如果您想在主线程上处理,您可以只使用+[NSRunLoop mainRunLoop]。 切换到使用基于计时器的dispatch source。这在 GCD 感知机制中实现了一个计时器,它将以您希望的间隔在您选择的队列上运行一个块。 在创建计时器之前显式地将dispatch_async() 返回到主队列。这相当于使用主运行循环的选项#1(因为它也会在主线程上创建计时器)。

当然,这里真正的问题是,你为什么要从 GCD 队列中创建一个计时器?

【讨论】:

Kevin 的第二个答案之后的另一种选择,我通过用 MSWeakTimer ( github.com/mindsnacks/MSWeakTimer ) 替换 NSTimer 解决了我的问题,并且刚刚通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0) 作为 dispatchQueue 。 很棒的总结。超级感谢。如果我理解正确: 1. 你是说没有 GCD 调度线程有一个主线程的运行循环 except 吗? 2. 如果为真,那么 dispatch_after 怎么样?如果没有运行循环,它将如何处理?还是它也是在 GCD 感知机制中实现的? @Honey Dispatch 线程没有运行循环。主线程不是调度线程(好吧,除非你使用dispatch_main(),如果是这样,它也没有运行循环,但你可能没有这样做)。主线程的 runloop 与 GCD 集成以显式排空主队列,作为 runloop 正常操作的一部分。 dispatch_after 是 GCD 的一部分(因此名称中有“dispatch”一词),它与 runloops 没有任何关系。 主线程是进程开始时唯一存在的线程。当 ios 应用程序调用 UIApplicationMain(如果您以正常方式设置应用程序委托,它会在后台执行此操作),它会在此线程上启动“主运行循环”。主运行循环一直运行到应用程序终止。主运行循环负责处理许多事情,包括所有 UI 事件,并且您的大部分代码(尤其是所有 UI 代码)都在主线程上运行,由主运行循环驱动。 调度线程是由 GCD 内部管理的线程,用于处理提交到调度队列的块。提交到主队列的块的特殊之处在于主运行循环自己处理它们,但提交到其他队列的块由 GCD 处理并在其中一个调度线程上执行。它们基本上只是工作线程。

以上是关于使用 GCD 运行重复 NSTimer?的主要内容,如果未能解决你的问题,请参考以下文章

Objective-C三种定时器CADisplayLink / NSTimer / GCD的使用

GCD dispatch_source基本使用,创建GCD定时器与NSTimer的区别

页面实现多个定时器(计时器)时选用NSTimer还是GCD

AVAudioPlayer 本地音频设置番外篇:后台定时修改音频音量NSTimer

页面实现多个定时器(计时器)时选用NSTimer还是GCD?(干货不湿)

IOS GCD定时器