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


69

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

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

Answers:


21

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

不管值多少钱,我认为通过使用Grand Central Dispatch生成新线程来处理计时器事件还是NSBlockOperation对您的主线程的完全合理使用。


我也更喜欢NSBlockOperations方法,除了它似乎没有调度到后台线程。它仅对添加的内容执行并发线程,但仅在当前线程中执行。
AWF4vk 2011年

@David:这取决于您将操作添加到哪个队列。如果将其添加到主队列,则它将在主线程上运行。如果将其添加到自己创建的队列中,则(至少在Mac上)它将在另一个线程上运行。
彼得·霍西

109

如果需要此功能,则在滚动视图(或地图)时计时器仍在运行,则需要将它们安排在其他运行循环模式下。替换当前的计时器:

[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];

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

编辑: 第二个代码块,NSTimer仍在主线程上运行,仍在与滚动视图相同的运行循环上。区别在于运行循环模式。查看博客文章以获取明确说明。


6
正确,这不是后台运行任务,而是在其他线程中运行计时器,而不是阻止UI。
Marius

6
该代码在这里很有意义,但是解释是错误的。mainRunLoop在main / UI线程上运行。您在这里所做的只是将其配置为从主线程以不同的模式运行。
史蒂芬·费舍尔

1
我试过了 这不在其他线程中运行。它在主线程中运行。因此,它阻止了UI操作。
2014年

1
@AnbuRaj它在主线程上运行,但不阻塞UI,因为它以不同的模式运行(如Steven Fisher所说)。另请参阅stackoverflow.com/a/7223765/4712
Marius

1
对于Swift:创建计时器:let timer = NSTimer.init(timeInterval: 0.1, target: self, selector: #selector(AClass.AMethod), userInfo: nil, repeats: true)接下来添加计时器:NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
Gerrit发布时间

53

如果您想使用纯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;
}

斯威夫特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()吗?都需要吗?
Neal Ehardt

@NealEhardt你明白了吗?我还需要在后台线程上使无效。我正在后台线程上执行并发操作,如果未完成,则需要超时。但是,如果他们确实完成,我需要使超时计时器无效。
horseshoe7

1
@ horseshoe7 -dispatch_suspend()并且dispatch_resume()将暂停和恢复这样的调度计时器。使用dispatch_source_cancel(),然后再进行删除dispatch_release()(在某些OS版本上,启用ARC的应用程序可能不需要后者),然后使之无效。
布拉德·拉尔森

我走了简单的路。这很好地解决了我的问题! github.com/mindsnacks/MSWeakTimer
horseshoe7

对我不起作用:(在第三行上说,此处不允许使用函数定义...
Roma-MT 2014年

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];
    });
}

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

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

希望能帮助到你


注意,这将在全局后台队列上而不是特定线程上运行该方法。这可能是OP想要的,也可能不是。
莉莉·巴拉德

4
而且,这将不会是完全准确的。计时器启动或方法处理中引入的任何延迟都将延迟下一个回调。dispatch_source如果您想使用GCD来驱动此功能,最好使用计时器。
莉莉·巴拉德

1
据我所知,@ HimanshuMahajan并不是一个单事件计时器,而是一个循环,然后停止。这就是为什么在methodToRepeatEveryOneSecondclass方法中必须再次重新启动它的原因。因此,如果您想停止它,则可以在该dispatch_queue_t ...行上方放置一个条件,以便在不想继续时返回。
Volomike

我使用的是命名管道IPC,需要后台循环进行检查。这个特殊的例程对我有用,而Marius的例程在那种情况下对我不起作用。
Volomike

13

对于Swift 3.0,

季霍夫的答案并不能解释太多。在这里增加了我的一些理解。

首先,下面是代码。它与我创建计时器的地方的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
    }
  }
}

创建队列

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

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

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

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

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

启动计时器

要启动计时器,请首先从DispatchQueue调用异步。然后,最好先检查计时器是否已经启动。如果计时器变量不是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()
  }
}

触发计时器

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

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可能不错。


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

5

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

计时器与运行循环结合使用,线程的运行循环只能从线程中获取,因此关键是线程中的调度计时器。

除主线程的运行循环外,运行循环应手动启动。在运行的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()
    })
}

计时器对目标有很强的引用,而运行循环对计时器有很强的引用,在计时器无效后,它释放目标,因此在目标中保留对它的弱引用,并在适当的时间使它无效,以退出运行循环(然后退出线程)。

注意:作为一种优化,sync函数DispatchQueue尽可能在当前线程上调用该块。实际上,您在主线程中执行了以上代码,在主线程中触发了Timer,因此请不要使用sync函数,否则,不会在您想要的线程上触发Timer。

您可以通过暂停在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中添加计时器,否则,将计时器添加到init调用者线程的运行循环中,而不是此线程的运行循环中,例如,您在主线程中运行以下代码,如果TimerThread在init方法中添加计时器,则计时器将被调度到主线程中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()

PS About Runloop.current.run(),其文件建议如果我们想让runloop终止,请不要调用此方法run(mode: RunLoopMode, before limitDate: Date),实际上run()在NSDefaultRunloopMode中反复调用此方法是什么模式?在runloop和thread中有更多详细信息。


2

我的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()
        }
    }
}

1

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

DispatchTimerhttps://github.com/arkdan/ARKExtensions中检出,该操作“在指定的调度队列上以指定的时间间隔,以指定的次数(可选)执行关闭。”

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

0
class BgLoop:Operation{
    func main(){
        while (!isCancelled) {
            sample();
            Thread.sleep(forTimeInterval: 1);
        }
    }
}

-1

如果您想让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。
阿德里安·拉贝(AdrianLabbé)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.