¿Cómo puedo crear un NSTimer en un hilo de fondo?


Tengo una tarea que debe realizarse cada 1 segundo. Actualmente tengo un NSTimer disparando repetidamente cada 1 segundo. ¿Cómo puedo hacer que el temporizador se dispare en un subproceso en segundo plano (que no es UI-thread)?

Podría tener el fuego NSTimer en el hilo principal y luego usar NSBlockOperation para enviar un hilo de fondo, pero me pregunto si hay una forma más eficiente de hacer esto.

Author: David, 2011-11-29

10 answers

El temporizador tendría que ser instalado en un bucle de ejecución que opera en un subproceso en segundo plano ya en ejecución. Ese hilo tendría que continuar ejecutando el bucle de ejecución para que el temporizador realmente se dispare. Y para que ese subproceso en segundo plano continúe siendo capaz de disparar otros eventos del temporizador, tendría que generar un nuevo subproceso para manejar realmente los eventos de todos modos (suponiendo, por supuesto, que el procesamiento que está haciendo toma una cantidad significativa de tiempo).

Para lo que valga, creo que el manejo eventos de temporizador al generar un nuevo hilo usando Grand Central Dispatch o NSBlockOperation es un uso perfectamente razonable de su hilo principal.

 17
Author: Steven Fisher,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-29 01:36:21

Si necesita esto para que los temporizadores se sigan ejecutando cuando desplace sus vistas (o mapas), debe programarlos en un modo de bucle de ejecución diferente. Reemplaza tu temporizador actual:

[NSTimer scheduledTimerWithTimeInterval:0.5
                                 target:self
                               selector:@selector(timerFired:)
                               userInfo:nil repeats:YES];

Con este:

NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
                                           target:self
                                         selector:@selector(timerFired:)
                                         userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Para obtener más información, consulte esta publicación del blog: El seguimiento de eventos detiene a NSTimer

EDITAR: segundo bloque de código, el NSTimer todavía se ejecuta en el hilo principal, todavía en el mismo bucle de ejecución que las vistas de desplazamiento. La diferencia es el modo run loop . Consulta el blog post para una explicación clara.

 100
Author: Marius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-06 10:03:58

Si quieres usar GCD puro y usar una fuente de despacho, Apple tiene un código de ejemplo para esto en su Guía de Programación de Concurrencia :

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
}

Entonces puedes configurar tu evento de temporizador de un segundo usando código como el siguiente:

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

Asegurándose de almacenar y liberar su temporizador cuando termine, por supuesto. Lo anterior le da un margen de 1/10 segundo en el disparo de estos eventos, que podría apretar si lo desea.

 47
Author: Brad Larson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-13 07:29:11

Esto debería funcionar,

Repite un método cada 1 segundo en una cola de fondo sin usar 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];
    });
}

Si está en la cola principal y desea llamar al método anterior, puede hacer esto para que cambie a una cola en segundo plano antes de que se ejecute:)

dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(q_background, ^{
    [self methodToRepeatEveryOneSecond];
});

Espero que ayude

 15
Author: nacho4d,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-29 01:48:18

Para swift 3.0,

La respuesta de Tikhonv no explica demasiado. Aquí se agrega algo de mi comprensión.

Para abreviar las cosas primero, aquí está el código. Es DIFERENTE del código de Tikhonv en el lugar donde creo el temporizador. Creo el temporizador usando constructer y lo agrego al bucle. Creo que la función scheduleTimer agregará el temporizador al RunLoop del hilo principal. Así que es mejor crear temporizador utilizando el constructor.

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

Crear Cola

Primero, cree una cola para hacer que timer se ejecute en segundo plano y almacene esa cola como una propiedad de clase para reutilizarla para detener timer. No estoy seguro de si necesitamos usar la misma cola para iniciar y detener, la razón por la que hice esto es porque vi un mensaje de advertencia aquí.

La clase RunLoop generalmente no se considera segura para subprocesos y sus métodos sólo deben ser llamados en el contexto de la actual hilo. Usted nunca debe tratar de llamar a la métodos de un objeto RunLoop correr en un hilo diferente, ya que hacerlo podría causar inesperados resultado.

Así que decidí almacenar la cola y usar la misma cola para el temporizador para evitar problemas de sincronización.

También crea un temporizador vacío y también se almacena en la variable de clase. Hágalo opcional para que pueda detener el temporizador y configurarlo en nil.

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

Temporizador de inicio

Para iniciar el temporizador, primero llame async desde DispatchQueue. Entonces es una buena práctica primero compruebe si el temporizador ya ha comenzado. Si el temporizador variable es no nulo, entonces invalidate() y nada.

El siguiente paso es obtener el RunLoop actual. Debido a que hicimos esto en el bloque de cola que creamos, obtendrá el RunLoop para la cola de fondo que creamos antes.

Crea el temporizador. Aquí, en lugar de usar scheduledTimer, simplemente llamamos al constructor de timer y pasamos cualquier propiedad que desee para el temporizador, como TimeInterval, target, selector, sucesivamente.

Agregue el temporizador creado al bucle de ejecución. Ponlo.

Aquí hay una pregunta sobre la ejecución del RunLoop. De acuerdo con la documentación aquí, dice que efectivamente comienza un bucle infinito que procesa datos de las fuentes de entrada y temporizadores del bucle de ejecución.

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

Temporizador de disparo

Implementa la función de forma normal. Cuando se llama a esa función, se llama bajo la cola de forma predeterminada.

func timerTriggered() {
  // under queue by default
  debug()
}

func debug() {
  let name = __dispatch_queue_get_label(nil)
  print(String(cString: name, encoding: .utf8))
}

La función de depuración anterior se usa para imprimir el nombre del cola. Si alguna vez te preocupa si se ha estado ejecutando en la cola, puedes llamarlo para verificar.

Detener el temporizador

Detener el temporizador es fácil, llame a validate() y establezca la variable timer almacenada dentro de la clase en nil.

Aquí lo estoy ejecutando bajo la cola de nuevo. Debido a la advertencia aquí, decidí ejecutar todo el código relacionado con el temporizador bajo la cola para evitar conflictos.

func stopTimer() {
  queue.sync { [unowned self] in
    guard let _ = self.timer else {
      // error, timer already stopped
      return
    }
    self.timer?.invalidate()
    self.timer = nil
  }
}

Preguntas relacionadas con RunLoop

De alguna manera estoy un poco confundido sobre si necesitamos detener manualmente el RunLoop o no. De acuerdo con la documentación aquí, parece que cuando no hay temporizadores conectados a ella, entonces saldrá inmediatamente. Así que cuando detengamos el temporizador, debería existir por sí mismo. Sin embargo, al final de ese documento, también dijo:

Eliminar todas las fuentes de entrada y temporizadores conocidos del bucle de ejecución no es un garantizar que el bucle de ejecución saldrá. macOS puede instalar y eliminar fuentes de entrada adicionales según sea necesario para procesar las solicitudes dirigidas a el hilo del receptor. Aquellos por lo tanto, las fuentes podrían evitar el bucle de ejecución de salir.

Probé la solución a continuación que se proporciona en la documentación para una garantía para terminar el bucle. Sin embargo, el temporizador no se dispara después de cambiar .ejecutar () al siguiente código.

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

Lo que estoy pensando es que podría ser seguro solo para usarlo .ejecutar () en iOS. Porque la documentación indica que macOS se instala y elimina fuentes de entrada adicionales según sea necesario para procesar las solicitudes dirigidas a hilo. Así que iOS podría estar bien.

 11
Author: nuynait,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-19 18:57:53

Mi solución Swift 3.0 para iOS 10+, timerMethod() será llamada en la cola en segundo plano.

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()
        }
    }
}
 2
Author: Tikhonov Alexander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-02 13:09:30

Solo Swift (aunque probablemente se pueda modificar para usarlo con Objective-C)

Echa un vistazo a DispatchTimer desde https://github.com/arkdan/ARKExtensions , que "Ejecuta un cierre en la cola de despacho especificada, con intervalos de tiempo especificados, para un número de veces especificado (opcionalmente). "

let queue = DispatchQueue(label: "ArbitraryQueue")
let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
    // body to execute until cancelled by timer.cancel()
}
 1
Author: user1244109,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-02 11:49:23

Hoy después de 6 años, trato de hacer lo mismo, aquí está la solución alternativa: GCD o NSThread.

Los temporizadores funcionan en conjunto con los bucles de ejecución, el runloop de un subproceso solo se puede obtener del subproceso, por lo que la clave es ese temporizador de programación en el subproceso.

Excepto el runloop del hilo principal, runloop debe iniciarse manualmente; debe haber algunos eventos para manejar en running runloop, como Timer, de lo contrario runloop saldrá, y podemos usar esto para salir de un runloop si timer es el único evento fuente: invalidar el temporizador.

El siguiente código es Swift 4:

Solución 0: DCG

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 tiene una fuerte referencia al objetivo, y runloop tiene una fuerte referencia al temporizador, después de invalidar el temporizador, libera el objetivo, por lo que mantén una débil referencia a él en el objetivo e invalídalo en el momento adecuado para salir de runloop(y luego salir del subproceso).

Nota: como optimización, la función sync de DispatchQueue invoca el bloque en el hilo actual cuando es posible. En realidad, tú ejecute el código anterior en el hilo principal, el temporizador se dispara en el hilo principal, así que no use la función sync, de lo contrario, el temporizador no se dispara en el hilo que desea.

Podría nombrar thread para rastrear su actividad pausando el programa que se ejecuta en Xcode. En DCG, uso:

Thread.current.name = "ThreadWithTimer"

Solución 1: Hilo

Podríamos usar NSThread directamente. No temas, el código es fácil.

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

Solución 2: Subproceso de subclase

Si quieres usar la subclase 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()
    }
}

Nota: no agregar temporizador en init, de lo contrario, timer es agregar al runloop del hilo que llama a init, no al runloop de este hilo, por ejemplo, se ejecuta el siguiente código en el hilo principal, si TimerThread agregar temporizador en el método init, timer se programará para el runloop del hilo principal, no para el runloop de timerThread. Puede verificarlo en 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. Sobre Runloop.current.run(), su documento sugiere no llamar a este método si queremos que runloop termine, use run(mode: RunLoopMode, before limitDate: Date), en realidad run() invoque repetidamente este método en el modo Nsdefaultrunloop, ¿qué es Mode? Más detalles en runloop y thread.

 1
Author: seedante,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-23 01:42:09
class BgLoop:Operation{
    func main(){
        while (!isCancelled) {
            sample();
            Thread.sleep(forTimeInterval: 1);
        }
    }
}
 0
Author: john07,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-06 10:49:11

Si desea que su NSTimer se ejecute en segundo plano, haga lo siguiente:

  1. call [self beginBackgroundTask] method in applicationWillResignActive methods
  2. call [self endBackgroundTask] method in applicationWillEnterForeground

Eso es todo

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

-(void)endBackgroundTask
{
    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}
 0
Author: Vishwas Singh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-06 10:05:07