Как мне создать NSTimer в фоновом потоке? - PullRequest
64 голосов
/ 29 ноября 2011

У меня есть задача, которую нужно выполнять каждую 1 секунду. В настоящее время я запускаю NSTimer раз в 1 сек. Как запустить таймер в фоновом потоке (не в UI-потоке)?

Я мог бы запустить NSTimer в главном потоке, а затем использовать NSBlockOperation для отправки фонового потока, но мне интересно, есть ли более эффективный способ сделать это.

Ответы [ 10 ]

106 голосов
/ 29 августа 2012

Если вам это нужно, чтобы таймеры продолжали работать при прокрутке ваших представлений (или карт), вам нужно запланировать их в другом режиме цикла выполнения. Замените свой текущий таймер:

[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 по-прежнему работает в главном потоке, в том же цикле выполнения, что и просмотры прокрутки. Разница заключается в цикле выполнения mode . Проверьте сообщение в блоге для четкого объяснения.

49 голосов
/ 06 декабря 2011

Если вы хотите использовать чистый 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;
}

Swift 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-ую свободу действий при запуске этих событий, которую вы можете усилить, если хотите.

19 голосов
/ 29 ноября 2011

Таймер должен быть установлен в цикл выполнения, работающий в уже запущенном фоновом потоке.Этот поток должен был бы продолжить выполнение цикла выполнения, чтобы таймер действительно сработал.И чтобы этот фоновый поток продолжал запускать другие события таймера, ему нужно было бы порождать новый поток для обработки событий в любом случае (при условии, конечно, что обработка, которую вы выполняете, занимает значительное количество времени).

Во что бы то ни стало, я думаю, что обработка событий таймера путем создания нового потока с помощью Grand Central Dispatch или NSBlockOperation - вполне разумное использование вашего основного потока.

15 голосов
/ 29 ноября 2011

Это должно работать,

Он повторяет метод каждую 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];
});

Надеюсь, это поможет

13 голосов
/ 19 апреля 2017

Для Swift 3.0,

Ответ Тихонва мало что объясняет. Здесь добавляет немного моего понимания.

Для краткости, вот код. Это РАЗНОЕ от кода Тихонва в том месте, где я создаю таймер. Я создаю таймер с помощью конструктора и добавляю его в цикл. Я думаю, что функция 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 RunTimer{
  let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
  let timer: Timer?
}

Таймер запуска

Чтобы запустить таймер, сначала вызовите async из DispatchQueue. Затем рекомендуется сначала проверить, запущен ли таймер. Если переменная таймера не равна nil, то invalidate () ее и установите в ноль.

Следующий шаг - получить текущий RunLoop. Поскольку мы сделали это в созданном нами блоке очереди, он получит RunLoop для фоновой очереди, которую мы создали ранее.

Создать таймер. Здесь вместо использования scheduleTimer мы просто вызываем конструктор 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 () и установите переменную таймера, хранящуюся в классе, в ноль.

Здесь я снова запускаю его в очереди. Из-за этого предупреждения я решил запустить весь связанный с таймером код в очереди, чтобы избежать конфликтов.

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

Я думаю, что было бы безопасно использовать просто .run () на iOS.Поскольку в документации говорится, что macOS устанавливается и удаляет дополнительные входные источники, необходимые для обработки запросов, направленных на поток получателя.Так что iOS может быть в порядке.

2 голосов
/ 28 февраля 2017

Решение My Swift 3.0 для iOS 10+, 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 голос
/ 23 января 2018

Сегодня, спустя 6 лет, я пытаюсь сделать то же самое, вот альтернативное решение: GCD или NSThread.

Таймеры работают в сочетании с циклами выполнения, цикл выполнения потока можно получить только из потока, поэтомуключ в том, что таймер расписания в потоке.

За исключением цикла выполнения основного потока, цикл запуска должен запускаться вручную;должны быть некоторые события для обработки в runloop, например Timer, иначе runloop завершится, и мы можем использовать это для выхода из runloop, если timer является единственным источником события: аннулировать таймер.

Следующий код - 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()
    })
}

Таймер имеет сильную ссылку на цель, а цикл запуска имеет сильную ссылку на таймер после того, как таймер недействителен, она освобождает цель, поэтому сохраняйте слабую ссылку на нее в target и аннулируйте ее в соответствующее время, чтобы выйти из runloop (и затем выйти из thread).

Примечание: в качестве оптимизации вызывается функция sync из DispatchQueueблок в текущем потоке, когда это возможно.На самом деле, вы выполняете вышеуказанный код в главном потоке, таймер запускается в основном потоке, поэтому не используйте функцию 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()
    }
}

Примечание: не делайтеt добавить таймер в init, в противном случае, таймер добавляется в runloop потока вызывающей стороны init, а не в runloop этого потока, например, вы запускаете следующий код в основном потоке, если TimerThread add timer в методе init, таймер будет запланирован на runloop основного потока, а не timerThread's runloop.Вы можете проверить это в timerMethod() log.

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 и thread .

1 голос
/ 02 февраля 2017

Swift only (хотя, вероятно, может быть изменено для использования с Objective-C)

Извлечь DispatchTimer из https://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 голосов
/ 06 апреля 2018

Если вы хотите, чтобы ваш NSTimer работал даже в фоновом режиме, сделайте следующее-

  1. вызов метода [self beginBackgroundTask] в методах applicationWillResignActive
  2. вызов метода [self endBackgroundTask] в applicationWillEnterForeground

Вот и все

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

-(void)endBackgroundTask
{
    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}
0 голосов
/ 06 февраля 2018
class BgLoop:Operation{
    func main(){
        while (!isCancelled) {
            sample();
            Thread.sleep(forTimeInterval: 1);
        }
    }
}
...