Как создать NSTimer в фоновом потоке?

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

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

Ответ 1

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

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

Ответ 2

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

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

Ответ 3

Если вы хотите перейти на чистый GCD и использовать источник отправки, у Apple есть пример кода для этого в Concurrency Руководство по программированию:

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

Ответ 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];
});

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

Ответ 5

Для быстрого 3.0,

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

Чтобы сделать вещи короткими, вот код. Это DIFFERENT из кода Тихонова в том месте, где я создаю таймер. Я создаю таймер с помощью конструктора и добавляю его в цикл. Я думаю, что функция schedTimer добавит таймер в основной поток 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 для ранее созданной очереди фона.

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

Ответ 6

Решение 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()
        }
    }
}

Ответ 7

Только Swift (хотя, вероятно, его можно изменить для использования с 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()
}