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

时间:2011-11-29 01:25:02

标签: objective-c cocoa nstimer nsrunloop nsblockoperation

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

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

10 个答案:

答案 0 :(得分:104)

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

[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仍然在主线程上运行,仍然在与scrollviews相同的运行循环中运行。不同之处在于运行循环模式。查看博客文章以获得清晰的解释。

答案 1 :(得分:49)

如果您想使用纯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秒余地,如果你愿意,可以收紧。

答案 2 :(得分:18)

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

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

答案 3 :(得分:15)

这应该有效,

它在后台队列中每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];
});

希望有所帮助

答案 4 :(得分:12)

对于swift 3.0,

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

首先简化,这是代码。在我创建计时器的地方,Tikhonv的代码是 DIFFERENT 。我使用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
    }
  }
}

创建队列

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

  

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

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

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

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

启动计时器

要启动计时器,首先从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()
  }
}

触发定时器

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

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可能没问题。

答案 5 :(得分:2)

适用于iOS 10+的My 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()
        }
    }
}

答案 6 :(得分:1)

仅限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()
}

答案 7 :(得分:1)

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

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

除主线程的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具有对目标的强引用,并且runloop具有对定时器的强引用,在定时器无效后,它释放目标,因此在目标中保持弱引用并在适当的时间使其无效以退出runloop(然后退出线程)。

注意:作为优化,sync DispatchQueue函数会在可能的情况下调用当前线程上的块。实际上,你在主线程中执行上面的代码,在主线程中触发Timer,所以不要使用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中添加计时器,否则,计时器会添加到初始调用者的线程的runloop中,而不是此线程的runloop,例如,你在主线程中运行以下代码,如果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关于Runloop.current.run(),如果我们希望runloop终止,请使用run(mode: RunLoopMode, before limitDate: Date),实际run()在NSDefaultRunloopMode中重复调用此方法,其文档建议不要调用此方法,什么模式? runloop and thread中的更多详细信息。

答案 8 :(得分:0)

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

答案 9 :(得分:-1)

如果您希望NSTimer在后台运行,请执行以下操作 -

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

    那是

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