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



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



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

696   10  

10 ответов:

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

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

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

[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

EDIT: второй блок кода, NSTimer все еще работает в основном потоке, все еще в том же цикле запуска, что и scrollviews. Разница заключается в прогоне петля режим. Проверьте сообщение в блоге для четкого объяснения.

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

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

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

надеюсь, что это помогает

для 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. Тогда это хорошая практика, чтобы сначала проверить, если таймер уже запущен. Если переменная timer не равна nil, то invalidate() IT и установите ее в nil.

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

создать таймер. Вот вместо того, чтобы использовать scheduledTimer, мы просто называем конструктор таймера и передать все, что вы хотите для таймера, такие как timeInterval, цель, селектор и т. д.

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

я попробовал Решение ниже, что предусмотрено в документации для гарантии завершения цикла. Однако таймер не срабатывает после того, как я меняю .запустите () в код ниже.

while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};

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

мое решение 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()
        }
    }
}

только 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()
}

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

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

кроме runloop основного потока, runloop должен запускаться вручную; должны быть некоторые события для обработки в running runloop, такие как таймер, иначе runloop выйдет, и мы можем использовать это для выхода из 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()
    })
}

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

Примечание: в качестве оптимизации, syncфункции DispatchQueue вызывает блок на текущий поток, когда это возможно. На самом деле, вы выполняете выше код в основном потоке, таймер запускается в основном потоке, так что не используйте sync функция, в противном случае таймер не срабатывает на поток, который вы хотите.

вы можете назвать поток для отслеживания его активности, приостановив выполнение программы в Xcode. В НОД, использовать:

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: Подкласс Нить

если вы хотите использовать подкласс потока:

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 вызывающего потока init, а не в runloop этого потока, например, вы запускаете следующий код в основном потоке, если TimerThread добавить таймер в метод init, таймер будет запланирован на runloop основного потока, а не runloop timerThread. Вы можете проверить это в 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()

P. S О Runloop.current.run(), его документ предлагает не вызывать этот метод если мы хотим завершить runloop, используйте run(mode: RunLoopMode, before limitDate: Date), на самом деле run() повторно вызвать этот метод в NSDefaultRunloopMode, что такое режим? Подробнее в runloop и thread.

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

Если вы хотите, чтобы ваш 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;
}

Comments

    Ничего не найдено.