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

Posted

技术标签:

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

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

我可以在主线程上触发 NSTimer,然后使用 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 所说)。另见***.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(然后退出线程)。

注意:作为优化,DispatchQueuesyncfunction 会尽可能调用当前线程上的块。实际上,你在主线程中执行上面的代码,定时器是在主线程中触发的,所以不要使用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 在均匀背景下运行,请执行以下操作-

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

就是这样

-(void)beginBackgroundTask

    bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
        [self endBackgroundTask];
    ];


-(void)endBackgroundTask

    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;

【讨论】:

我认为您误解了这个问题。他想在后台线程上运行时间,但不一定在应用程序的后台状态上运行。另外,由于 Cocoa 标签,问题是关于 macOS 的。

以上是关于如何在后台线程上创建 NSTimer?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 sizeForItemAt 放在后台线程上?

在后台线程上创建 UIViewController 可以吗?

在私有/后台队列上创建 NSManagedObjectContext:怎么办?

在后台线程上创建一个视图,将其添加到主线程上的主视图

iOS 在后台保存主线程 NSManagedObjectContext 更改

我无法使用非主 MOC 在后台线程上创建 NSManagedObject 的新实例