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

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

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

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


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

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

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
    }
  }
}

Создать Очередь

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

класс RunLoop обычно не считается потокобезопасным и его методы должны вызываться только в пределах контекст текущего нитка. Вы никогда не должны пытаться вызвать методы объекта RunLoop запущена в другом потоке, так как это может привести к неожиданным результаты.

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

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

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

старт Таймер

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

что Я думаю, что это может быть безопасным для использования .запустить() на 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 лет я пытаюсь сделать то же самое, вот альтернативный soltion: GCD или NSThread.

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

кроме runloop основного потока, runloop должен запускаться вручную; должны быть некоторые события для обработки в 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. В 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: Подкласс Нить

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

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()

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


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