如何在后台线程上创建NSTimer?

Dav*_*vid 66 cocoa objective-c nstimer nsrunloop nsblockoperation

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

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

Mar*_*ius 107

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

[NSTimer scheduledTimerWithTimeInterval:0.5
                                 target:self
                               selector:@selector(timerFired:)
                               userInfo:nil repeats:YES];
Run Code Online (Sandbox Code Playgroud)

有了这个:

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

有关详细信息,请查看此博文:事件跟踪会停止NSTimer

编辑: 第二块代码,NSTimer仍然在主线程上运行,仍然在与scrollviews相同的运行循环中.不同之处在于运行循环模式.查看博客文章以获得清晰的解释.

  • 正确,这不是后台运行任务,而是在不同的线程中运行计时器,而不是阻止UI. (6认同)
  • 代码在这里有意义,但解释是错误的.mainRunLoop在主/ UI线程上运行.你在这里所做的就是将它配置为在不同模式下从主线程运行. (5认同)
  • 当应用程序在后台运行时,它不起作用 (4认同)
  • 我试过这个。这不是在不同的线程中运行。它在主线程中运行。因此它会阻止 UI 操作。 (2认同)

Bra*_*son 50

如果你想使用纯GCD并使用调度源,Apple在他们的并发编程指南中有一些示例代码:

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;
}
Run Code Online (Sandbox Code Playgroud)

斯威夫特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
}
Run Code Online (Sandbox Code Playgroud)

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

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
});
Run Code Online (Sandbox Code Playgroud)

当然,确保在完成后存储和释放你的计时器.上面给出了这些事件发射的1/10秒余地,如果你愿意,你可以收紧.

  • @horseshoe7 - `dispatch_suspend()` 和 `dispatch_resume()` 将暂停和恢复这样的调度计时器。使用`dispatch_source_cancel()` 和`dispatch_release()` 完成移除前的失效(后者对于某些操作系统版本上启用ARC 的应用程序可能不是必需的)。 (2认同)

Ste*_*her 19

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

无论它值多少,我认为通过使用Grand Central Dispatch生成一个新线程来处理计时器事件,或者NSBlockOperation是完全合理地使用你的主线程.


nac*_*o4d 16

这应该工作,

它在后台队列中每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];
    });
}
Run Code Online (Sandbox Code Playgroud)

如果您在主队列中并且想要调用上面的方法,则可以执行此操作,以便在运行之前将其更改为后台队列:)

dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(q_background, ^{
    [self methodToRepeatEveryOneSecond];
});
Run Code Online (Sandbox Code Playgroud)

希望能帮助到你

  • 而且,这不是完全准确的.在计时器触发或方法处理中引入的任何延迟都将延迟下一次回调.如果你想使用GCD来驱动这个东西,最好使用`dispatch_source`计时器. (4认同)
  • 如何阻止这个后台线程进程? (3认同)
  • @HimanshuMahajan 据我所知,它实际上并不是一个循环,而是一个单事件计时器,然后它就停止了。这就是为什么在 `methodToRepeatEveryOneSecond` 类方法中必须再次重新启动它。因此,如果您想停止它,可以在“dispatch_queue_t ...”行上方放置一个条件,以便在不想继续时返回。 (2认同)

nuy*_*ait 13

对于swift 3.0,

Tikhonv的答案并没有解释太多.这增加了我的一些理解.

为了简化事情,这里是代码.它与我创建计时器的地方的Tikhonv代码不同.我使用constructer创建计时器并将其添加到循环中.我认为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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

创建队列

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

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

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

还要创建一个空计时器并存储在类变量中.使其成为可选项,以便您可以停止计时器并将其设置为零.

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

启动计时器

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

下一步是获取当前的RunLoop.因为我们在我们创建的队列块中执行了此操作,所以它将为我们之前创建的后台队列获取RunLoop.

创建计时器.这里不是使用scheduledTimer,而是调用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()
  }
}
Run Code Online (Sandbox Code Playgroud)

触发定时器

正常实现功能.调用该函数时,默认情况下会在队列下调用它.

func timerTriggered() {
  // under queue by default
  debug()
}

func debug() {
  let name = __dispatch_queue_get_label(nil)
  print(String(cString: name, encoding: .utf8))
}
Run Code Online (Sandbox Code Playgroud)

上面的调试函数用于打印出队列的名称.如果您担心它是否已在队列中运行,您可以调用它进行检查.

停止计时器

停止计时器很简单,调用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
  }
}
Run Code Online (Sandbox Code Playgroud)

与RunLoop相关的问题

如果我们需要手动停止RunLoop,我有点困惑.根据这里的文档,似乎没有定时器附加它,它会立即退出.因此,当我们停止计时器时,它应该存在.但是,在该文件的最后,它还说:

从运行循环中删除所有已知的输入源和计时器并不能保证运行循环将退出.macOS可以根据需要安装和删除其他输入源,以处理针对接收者线程的请求.因此,这些来源可以防止运行循环退出.

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

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

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

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

see*_*nte 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()
    })
}
Run Code Online (Sandbox Code Playgroud)

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

注意:作为优化,sync函数DispatchQueue在可能的情况下调用当前线程上的块。实际上,您在主线程中执行上述代码, Timer 在主线程中被触发,所以不要使用sync函数,否则 timer 不会在您想要的线程中被触发。

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

Thread.current.name = "ThreadWithTimer"
Run Code Online (Sandbox Code Playgroud)

解决方案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()
}
Run Code Online (Sandbox Code Playgroud)

解决方案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()
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:不要在init里面加timer,否则timer是加到init调用者线程的runloop中的,不是这个线程的runloop,比如你在主线程中运行下面的代码,如果TimerThread在init方法中加了timer,timer会被调度到主线程的运行循环,而不是 timerThread 的运行循环。您可以在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()
Run Code Online (Sandbox Code Playgroud)

PS About Runloop.current.run(),它的文档建议如果我们想要runloop终止就不要调用这个方法,使用run(mode: RunLoopMode, before limitDate: Date),实际上run()在NSDefaultRunloopMode中重复调用这个方法,什么是模式?runloop 和 thread 中有更多细节。