【问题标题】:How do I create a NSTimer on a background thread?如何在后台线程上创建 NSTimer?
【发布时间】:2012-01-08 10:26:28
【问题描述】:

我有一个需要每 1 秒执行一次的任务。目前我有一个 NSTimer 每 1 秒重复触发一次。如何在后台线程(非 UI 线程)中触发计时器?

我可以在主线程上触发 NSTimer,然后使用 NSBlockOperation 调度后台线程,但我想知道是否有更有效的方法来执行此操作。

【问题讨论】:

    标签: objective-c cocoa nstimer nsrunloop nsblockoperation


    【解决方案1】:

    如果您需要这样做,以便在您滚动视图(或地图)时计时器仍然运行,您需要将它们安排在不同的运行循环模式下。替换您当前的计时器:

    [NSTimer scheduledTimerWithTimeInterval:0.5
                                     target:self
                                   selector:@selector(timerFired:)
                                   userInfo:nil repeats:YES];
    

    有了这个:

    NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
                                               target:self
                                             selector:@selector(timerFired:)
                                             userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    详情请查看这篇博文:Event tracking stops NSTimer

    编辑: 第二个代码块,NSTimer 仍然在主线程上运行,仍然在与滚动视图相同的运行循环中。区别在于运行循环模式。查看博客文章以获得清晰的解释。

    【讨论】:

    • 正确,这不是后台运行任务,而是在不同的线程中运行定时器,而不是阻塞UI。
    • 这里的代码是有道理的,但是解释是错误的。 mainRunLoop 在主/UI 线程上运行。您在这里所做的只是将其配置为在不同模式下从主线程运行。
    • 我试过这个。这不是在不同的线程中运行。它在主线程中运行。所以它会阻止 UI 操作。
    • @AnbuRaj 它在主线程上运行,但没有阻塞 UI,因为它以不同的模式运行(正如 Steven Fisher 所说)。另见stackoverflow.com/a/7223765/4712
    • 对于 Swift:创建计时器:let timer = NSTimer.init(timeInterval: 0.1, target: self, selector: #selector(AClass.AMethod), userInfo: nil, repeats: true) 接下来添加计时器:NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
    【解决方案2】:

    如果您想使用纯 GCD 并使用调度源,Apple 在其Concurrency Programming Guide 中有一些示例代码:

    dispatch_source_t CreateDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue, dispatch_block_t block)
    {
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        if (timer)
        {
            dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
            dispatch_source_set_event_handler(timer, block);
            dispatch_resume(timer);
        }
        return timer;
    }
    

    斯威夫特 3:

    func createDispatchTimer(interval: DispatchTimeInterval,
                             leeway: DispatchTimeInterval,
                             queue: DispatchQueue,
                             block: @escaping ()->()) -> DispatchSourceTimer {
        let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0),
                                                   queue: queue)
        timer.scheduleRepeating(deadline: DispatchTime.now(),
                                interval: interval,
                                leeway: leeway)
    
        // Use DispatchWorkItem for compatibility with iOS 9. Since iOS 10 you can use DispatchSourceHandler
        let workItem = DispatchWorkItem(block: block)
        timer.setEventHandler(handler: workItem)
        timer.resume()
        return timer
    }
    

    然后您可以使用如下代码设置一秒计时器事件:

    dispatch_source_t newTimer = CreateDispatchTimer(1ull * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // Repeating task
    });
    

    当然,请确保在完成后存储和释放您的计时器。以上为您在触发这些事件时提供了 1/10 秒的余地,如果您愿意,可以收紧。

    【讨论】:

    • 我喜欢 NSTimer 因为它的invalidate 方法。我会使用dispatch_suspend() 或dispatch_source_cancel() 得到类似的行为吗?两者都需要吗?
    • @NealEhardt 你明白了吗?我还需要在后台线程上无效。我正在后台线程上执行并发操作,如果它们没有完成,它们需要超时。但如果他们完成了,我需要使超时计时器无效。
    • @horseshoe7 - dispatch_suspend() 和 dispatch_resume() 将像这样暂停和恢复调度计时器。使用dispatch_source_cancel() 和dispatch_release() 完成删除前的失效操作(在某些操作系统版本上启用 ARC 的应用程序可能不需要后者)。
    • 我采取了简单的方法。这很好地解决了我的问题! github.com/mindsnacks/MSWeakTimer
    • 对我不起作用 :( 在第 3 行说这里不允许函数定义 ...
    【解决方案3】:

    需要将计时器安装到在已运行的后台线程上运行的运行循环中。该线程必须继续运行运行循环才能真正触发计时器。为了让该后台线程继续能够触发其他计时器事件,它需要生成一个新线程来实际处理事件(当然,假设您正在进行的处理需要大量时间)。

    无论如何,我认为通过使用 Grand Central Dispatch 或 NSBlockOperation 生成新线程来处理计时器事件是对主线程的完全合理的使用。

    【讨论】:

    • 我也更喜欢 NSBlockOperations 方法,除了它似乎没有调度到后台线程。它只为添加的内容执行并发线程,但只在当前线程中。
    • @David:这取决于您将操作添加到哪个队列。如果将其添加到主队列,那么它将在主线程上运行。如果您将其添加到您自己创建的队列中,那么(至少在 Mac 上)它将在另一个线程上运行。
    【解决方案4】:

    这应该可行,

    它在后台队列中每 1 秒重复一个方法,而不使用 NSTimers :)

    - (void)methodToRepeatEveryOneSecond
    {
        // Do your thing here
    
        // Call this method again using GCD 
        dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
        double delayInSeconds = 1.0;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
        dispatch_after(popTime, q_background, ^(void){
            [self methodToRepeatEveryOneSecond];
        });
    }
    

    如果您在主队列中并且想要调用上述方法,您可以这样做,以便在运行之前更改为后台队列:)

    dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_async(q_background, ^{
        [self methodToRepeatEveryOneSecond];
    });
    

    希望对你有帮助

    【讨论】:

    • 注意,这会在全局后台队列上运行该方法,而不是在特定线程上运行。这可能是也可能不是 OP 想要的。
    • 另外,这并不完全准确。定时器触发或方法处理中引入的任何延迟都会延迟下一次回调。如果您想使用 GCD 为这东西供电,最好使用dispatch_source 计时器。
    • @HimanshuMahajan 据我所知,它不像单事件计时器那样真正是一个循环,然后它就会停止。这就是为什么在methodToRepeatEveryOneSecond 类方法中必须重新启动它。因此,如果您想停止它,您可以在dispatch_queue_t ... 行上方放置一个条件,以便在您不想继续时返回。
    • 我使用的是命名管道 IPC,需要一个后台循环来检查它。这个特殊的程序对我有用,而来自Marius 的程序在这种情况下对我不起作用。
    【解决方案5】:

    对于 swift 3.0,

    Tikhonv 的回答并没有过多解释。这里补充一些我的理解。

    为了简短起见,这里是代码。在我创建计时器的地方,它与 Tikhonv 的代码不同。我使用构造函数创建计时器并将其添加到循环中。我认为 scheduleTimer 函数会将计时器添加到主线程的 RunLoop 中。所以最好使用构造函数来创建定时器。

    class RunTimer{
      let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
      let timer: Timer?
    
      private func startTimer() {
        // schedule timer on background
        queue.async { [unowned self] in
          if let _ = self.timer {
            self.timer?.invalidate()
            self.timer = nil
          }
          let currentRunLoop = RunLoop.current
          self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
          currentRunLoop.add(self.timer!, forMode: .commonModes)
          currentRunLoop.run()
        }
      }
    
      func timerTriggered() {
        // it will run under queue by default
        debug()
      }
    
      func debug() {
         // print out the name of current queue
         let name = __dispatch_queue_get_label(nil)
         print(String(cString: name, encoding: .utf8))
      }
    
      func stopTimer() {
        queue.sync { [unowned self] in
          guard let _ = self.timer else {
            // error, timer already stopped
            return
          }
          self.timer?.invalidate()
          self.timer = nil
        }
      }
    }
    

    创建队列

    首先,创建一个队列以使计时器在后台运行,并将该队列存储为类属性,以便将其重用于停止计时器。我不确定我们是否需要使用相同的队列来启动和停止,我这样做的原因是因为我看到了一条警告消息here。

    RunLoop 类通常不被认为是线程安全的,并且 它的方法只能在当前的上下文中调用 线。您永远不应该尝试调用 RunLoop 对象的方法 在不同的线程中运行,因为这样做可能会导致意外 结果。

    所以我决定存储队列并为计时器使用相同的队列以避免同步问题。

    还创建一个空计时器并将其存储在类变量中。将其设为可选,以便您可以停止计时器并将其设置为 nil。

    class RunTimer{
      let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
      let timer: Timer?
    }
    

    启动计时器

    要启动计时器,首先从 DispatchQueue 调用 async。然后最好先检查计时器是否已经启动。如果计时器变量不为 nil,则 invalidate() 并将其设置为 nil。

    下一步是获取当前的 RunLoop。因为我们是在我们创建的队列块中这样做的,所以它将获得我们之前创建的后台队列的 RunLoop。

    创建计时器。这里不使用 scheduleTimer,而是调用 timer 的构造函数,并传入任何你想要的 timer 属性,例如 timeInterval、target、selector 等。

    将创建的计时器添加到 RunLoop。运行它。

    这是一个关于运行 RunLoop 的问题。根据此处的文档,它说它有效地开始了一个无限循环,该循环处理来自运行循环的输入源和计时器的数据。

    private func startTimer() {
      // schedule timer on background
      queue.async { [unowned self] in
        if let _ = self.timer {
          self.timer?.invalidate()
          self.timer = nil
        }
    
        let currentRunLoop = RunLoop.current
        self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
        currentRunLoop.add(self.timer!, forMode: .commonModes)
        currentRunLoop.run()
      }
    }
    

    触发定时器

    正常执行功能。调用该函数时,默认在队列下调用。

    func timerTriggered() {
      // under queue by default
      debug()
    }
    
    func debug() {
      let name = __dispatch_queue_get_label(nil)
      print(String(cString: name, encoding: .utf8))
    }
    

    上面的调试功能是用来打印出队列的名字的。如果你担心它是否已经在队列中运行,你可以调用它来检查。

    停止计时器

    停止计时器很简单,调用 validate() 并将存储在类中的计时器变量设置为 nil。

    在这里,我再次在队列下运行它。由于这里的警告,我决定在队列下运行所有​​与计时器相关的代码以避免冲突。

    func stopTimer() {
      queue.sync { [unowned self] in
        guard let _ = self.timer else {
          // error, timer already stopped
          return
        }
        self.timer?.invalidate()
        self.timer = nil
      }
    }
    

    与 RunLoop 相关的问题

    我对是否需要手动停止 RunLoop 感到有些困惑。根据此处的文档,似乎当没有附加计时器时,它将立即退出。因此,当我们停止计时器时,它应该自身存在。但是,在该文件的末尾,它还说:

    从运行循环中删除所有已知的输入源和计时器并不是 保证运行循环将退出。 macOS 可以安装和删除 处理针对 接收者的线程。因此,这些来源可能会阻止运行循环 从退出。

    我尝试了文档中提供的以下解决方案,以保证终止循环。但是,将 .run() 更改为下面的代码后,计时器不会触发。

    while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};
    

    我的想法是在 iOS 上使用 .run() 可能是安全的。因为文档指出 macOS 会根据需要安装和删除其他输入源,以处理针对接收者线程的请求。所以iOS可能没问题。

    【讨论】:

    • 请记住,队列不是线程,两个队列实际上可能在同一个线程上运行(甚至在主线程上)。考虑到Timer 绑定到绑定到线程而不是队列的RunLoop,这可能会导致意外问题。
    【解决方案6】:

    6 年后的今天,我尝试做同样的事情,这里是替代解决方案:GCD 或 NSThread。

    定时器与run loops一起工作,一个线程的runloop只能从线程中获取,所以关键是线程中的调度定时器。

    除了主线程的runloop,runloop应该手动启动;在运行runloop中应该有一些事件需要处理,比如Timer,否则runloop会退出,如果timer是唯一的事件源,我们可以使用它来退出runloop:使定时器失效。

    以下代码是 Swift 4:

    解决方案 0:GCD

    weak var weakTimer: Timer?
    @objc func timerMethod() {
        // vefiry whether timer is fired in background thread
        NSLog("It's called from main thread: \(Thread.isMainThread)")
    }
    
    func scheduleTimerInBackgroundThread(){
        DispatchQueue.global().async(execute: {
            //This method schedules timer to current runloop.
            self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
            //start runloop manually, otherwise timer won't fire
            //add timer before run, otherwise runloop find there's nothing to do and exit directly.
            RunLoop.current.run()
        })
    }
    

    Timer对target有强引用,runloop对timer有强引用,定时器失效后释放target,所以在target中保持对它的弱引用,并在适当的时候使其失效退出runloop(然后退出线程)。

    注意:作为优化,DispatchQueue 的syncfunction 会尽可能调用当前线程上的块。实际上,你在主线程中执行上面的代码,定时器是在主线程中触发的,所以不要使用sync函数,否则定时器不会在你想要的线程中触发。

    您可以通过暂停在 Xcode 中执行的程序来命名线程以跟踪其活动。在 GCD 中,使用:

    Thread.current.name = "ThreadWithTimer"
    

    解决方案 1:线程

    我们可以直接使用 NSThread。别怕,写代码很简单。

    func configurateTimerInBackgroundThread(){
        // Don't worry, thread won't be recycled after this method return.
        // Of course, it must be started.
        let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil)
        thread.start()
    }
    
    @objc func addTimer() {
        weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
        RunLoop.current.run()
    }
    

    解决方案 2:子类线程

    如果你想使用 Thread 子类:

    class TimerThread: Thread {
        var timer: Timer
        init(timer: Timer) {
            self.timer = timer
            super.init()
        }
    
        override func main() {
            RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
            RunLoop.current.run()
        }
    }
    

    注意:不要在init中添加timer,否则timer会添加到init的调用者线程的runloop,而不是这个线程的runloop,例如,你在主线程中运行以下代码,如果TimerThread在init方法中添加timer,timer会被调度到主线程的runloop,而不是timerThread的runloop。您可以在timerMethod() 日志中进行验证。

    let timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
    weakTimer = timer
    let timerThread = TimerThread.init(timer: timer)
    timerThread.start()
    

    P.S 关于Runloop.current.run(),它的文档建议如果我们想终止runloop,不要调用这个方法,使用run(mode: RunLoopMode, before limitDate: Date),实际上run()在NSDefaultRunloopMode中反复调用这个方法,什么模式?更多详情runloop and thread。

    【讨论】:

      【解决方案7】:

      我的 iOS 10+ Swift 3.0 解决方案,timerMethod() 将在后台队列中调用。

      class ViewController: UIViewController {
      
          var timer: Timer!
          let queue = DispatchQueue(label: "Timer DispatchQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
      
          override func viewDidLoad() {
              super.viewDidLoad()
      
              queue.async { [unowned self] in
                  let currentRunLoop = RunLoop.current
                  let timeInterval = 1.0
                  self.timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timerMethod), userInfo: nil, repeats: true)
                  self.timer.tolerance = timeInterval * 0.1
                  currentRunLoop.add(self.timer, forMode: .commonModes)
                  currentRunLoop.run()
              }
          }
      
          func timerMethod() {
              print("code")
          }
      
          override func viewDidDisappear(_ animated: Bool) {
              super.viewDidDisappear(animated)
              queue.sync {
                  timer.invalidate()
              }
          }
      }
      

      【讨论】:

        【解决方案8】:

        仅适用于 Swift(尽管可以修改为与 Objective-C 一起使用)

        从https://github.com/arkdan/ARKExtensions 中查看DispatchTimer,它“以指定的时间间隔在指定的调度队列上执行闭包指定的次数(可选)。”

        let queue = DispatchQueue(label: "ArbitraryQueue")
        let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
            // body to execute until cancelled by timer.cancel()
        }
        

        【讨论】:

          【解决方案9】:
          class BgLoop:Operation{
              func main(){
                  while (!isCancelled) {
                      sample();
                      Thread.sleep(forTimeInterval: 1);
                  }
              }
          }
          

          【讨论】:

            【解决方案10】:

            如果您希望您的 NSTimer 在均匀背景下运行,请执行以下操作-

            1. 在 applicationWillResignActive 方法中调用 [self beginBackgroundTask] 方法
            2. 在applicationWillEnterForeground中调用[self endBackgroundTask]方法

            就是这样

            -(void)beginBackgroundTask
            {
                bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
                    [self endBackgroundTask];
                }];
            }
            
            -(void)endBackgroundTask
            {
                [[UIApplication sharedApplication] endBackgroundTask:bgTask];
                bgTask = UIBackgroundTaskInvalid;
            }
            

            【讨论】:

            • 我认为您误解了这个问题。他想在后台线程上运行时间,但不一定在应用程序的后台状态上运行。另外,由于 Cocoa 标签,问题是关于 macOS 的。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-06-22
            • 1970-01-01
            • 2014-04-02
            • 1970-01-01
            • 2012-09-01
            相关资源
            最近更新 更多