Comment créer un NSTimer sur un thread d’arrière-plan?

J’ai une tâche à effectuer toutes les 1 secondes. Actuellement, j’ai un NSTimer déclenché plusieurs fois par seconde. Comment puis-je déclencher le minuteur dans un thread d’arrière-plan (non-interface utilisateur)?

Je pourrais avoir le feu NSTimer sur le thread principal, puis utiliser NSBlockOperation pour envoyer un thread d’arrière-plan, mais je me demande s’il existe un moyen plus efficace de le faire.

Le minuteur devrait être installé dans une boucle d’exécution fonctionnant sur un thread d’arrière-plan déjà en cours d’exécution. Ce thread devrait continuer à exécuter la boucle d’exécution pour que le minuteur se déclenche réellement. Et pour que ce thread d’arrière-plan continue à pouvoir déclencher d’autres événements de temporisation, il devrait générer un nouveau thread pour gérer les événements de toute façon (en supposant, bien sûr, que le traitement que vous effectuez prenne beaucoup de temps).

Pour tout ce que cela vaut, je pense que la gestion des événements du minuteur en créant un nouveau thread en utilisant Grand Central Dispatch ou NSBlockOperation est une utilisation parfaitement raisonnable de votre thread principal.

Si vous en avez besoin pour que les timers continuent de fonctionner lorsque vous faites défiler vos vues (ou vos cartes), vous devez les planifier en mode de boucle d’exécution différent. Remplacez votre timer actuelle:

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

Avec celui-ci:

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

Pour plus de détails, consultez cet article: Le suivi des événements arrête NSTimer

EDIT: deuxième bloc de code, le NSTimer fonctionne toujours sur le thread principal, toujours sur la même boucle d’exécution que les scrollviews. La différence est le mode de boucle d’exécution. Consultez le blog pour une explication claire.

Si vous souhaitez utiliser du GCD pur et utiliser une source de répartition, Apple propose un exemple de code pour cela dans son Guide de programmation simultanée :

 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 } 

Vous pouvez ensuite configurer votre événement de timer d’une seconde en utilisant le code suivant:

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

assurez-vous de stocker et de libérer votre timer une fois terminé, bien sûr. Ce qui précède vous donne une marge de 1 / 10ème de seconde sur le déclenchement de ces événements, que vous pouvez resserrer si vous le souhaitez.

Cela devrait fonctionner,

Il répète une méthode toutes les secondes dans une queue en arrière-plan sans utiliser 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 vous êtes dans la queue principale et que vous souhaitez appeler la méthode ci-dessus, vous pouvez le faire pour qu’elle passe à une queue d’arrière-plan avant son exécution 🙂

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

J’espère que cela aide

Pour swift 3.0,

La réponse de Tikhonv n’explique pas trop. Ici ajoute une partie de ma compréhension.

Pour rendre les choses courtes, voici le code. Il est différent du code de Tikhonv à l’endroit où je crée la timer. Je crée la timer en utilisant le constructeur et l’ajoute dans la boucle. Je pense que la fonction scheduleTimer appenda la timer au RunLoop du thread principal. Il est donc préférable de créer un temporisateur en utilisant le constructeur.

 class RunTimer{ let queue = DispatchQueue(label: "Timer", qos: .background, atsortingbutes: .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(Ssortingng(cSsortingng: 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 } } } 

Créer une queue

Tout d’abord, créez une queue pour que le minuteur soit exécuté en arrière-plan et stockez cette queue en tant que propriété de classe afin de pouvoir la réutiliser pour l’arrêt du minuteur. Je ne suis pas sûr que nous devions utiliser la même file pour le démarrage et l’arrêt, parce que j’ai vu un message d’avertissement ici .

La classe RunLoop n’est généralement pas considérée comme étant thread-safe et ses méthodes ne doivent être appelées que dans le contexte du thread en cours. Vous ne devez jamais essayer d’appeler les méthodes d’un object RunLoop s’exécutant dans un thread différent, car cela pourrait entraîner des résultats inattendus.

J’ai donc décidé de stocker la queue et d’utiliser la même queue pour le minuteur afin d’éviter les problèmes de synchronisation.

Créez également une timer vide et stockée dans la variable de classe. Rendez-le facultatif pour pouvoir arrêter le minuteur et le mettre à zéro.

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

Début du minuteur

Pour démarrer le minuteur, appelez d’abord async à partir de DispatchQueue. Ensuite, il est conseillé de vérifier au préalable si la timer a déjà démarré. Si la variable timer n’est pas nulle, invalidez-la et mettez-la à nil.

L’étape suivante consiste à obtenir le RunLoop actuel. Comme nous l’avons fait dans le bloc de queue que nous avons créé, il obtiendra le RunLoop pour la queue d’arrière-plan que nous avons créée auparavant.

Créez la timer. Ici, au lieu d’utiliser scheduleTimer, nous appelons simplement le constructeur de timer et transmettons la propriété de votre choix pour le timer, par exemple timeInterval, target, selector, etc.

Ajoutez le minuteur créé au RunLoop. Exécuter.

Voici une question sur l’exécution du RunLoop. Selon la documentation présentée ici, il est indiqué qu’elle commence effectivement une boucle infinie qui traite les données provenant des sources d’entrée et des temporisateurs de la boucle d’exécution.

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

Minuterie de déclenchement

Implémentez la fonction normalement. Lorsque cette fonction est appelée, elle est appelée dans la queue par défaut.

 func timerTriggered() { // under queue by default debug() } func debug() { let name = __dispatch_queue_get_label(nil) print(Ssortingng(cSsortingng: name, encoding: .utf8)) } 

La fonction de débogage ci-dessus est utilisée pour imprimer le nom de la queue. Si jamais vous vous inquiétez s’il a été exécuté dans la queue, vous pouvez l’appeler pour vérifier.

Arrêt timer

Stop timer est facile, appelez validate () et définissez la variable timer stockée dans la classe sur nil.

Ici, je l’exécute sous la queue à nouveau. En raison de l’avertissement ici, j’ai décidé d’exécuter tout le code lié à la timer sous la queue pour éviter les conflits.

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

Questions liées à RunLoop

Je suis un peu confus si nous devons arrêter manuellement le RunLoop ou non. Selon la documentation ici, il semble que quand aucun minuteur ne s’y rattache, alors il va quitter immédiatement. Donc, quand on arrête le chronomètre, il devrait exister lui-même. Cependant, à la fin de ce document, il a également déclaré:

La suppression de toutes les sources d’entrée et de tous les temporisateurs connus de la boucle d’exécution ne garantit pas la fermeture de la boucle d’exécution. macOS peut installer et supprimer des sources d’entrée supplémentaires pour traiter les requêtes ciblées sur le thread du destinataire. Ces sources pourraient donc empêcher la boucle d’exécution de sortir.

J’ai essayé la solution ci-dessous qui est fournie dans la documentation pour une garantie de terminer la boucle. Cependant, le minuteur ne se déclenche pas après avoir changé .run () pour le code ci-dessous.

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

Ce que je pense, c’est qu’il pourrait être prudent d’utiliser simplement .run () sur iOS. Parce que la documentation indique que macOS est installé et supprime les sources d’entrée supplémentaires nécessaires pour traiter les requêtes ciblées sur le thread du destinataire. Donc, iOS pourrait être bien.

Ma solution Swift 3.0 pour iOS 10+, timerMethod() sera appelée dans la queue en arrière-plan.

 class ViewController: UIViewController { var timer: Timer! let queue = DispatchQueue(label: "Timer DispatchQueue", qos: .background, atsortingbutes: .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 uniquement (bien que cela puisse probablement être modifié pour être utilisé avec Objective-C)

Découvrez DispatchTimer partir de https://github.com/arkdan/ARKExtensions , qui “Exécute une fermeture sur la queue de dissortingbution spécifiée, avec des intervalles de temps spécifiés, pour le nombre de fois spécifié (éventuellement).”

 let queue = DispatchQueue(label: "ArbitraryQueue") let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in // body to execute until cancelled by timer.cancel() } 

Aujourd’hui, après 6 ans, j’essaie de faire la même chose, voici une solution alternative: GCD ou NSThread.

Les timers fonctionnent conjointement avec des boucles d’exécution, la boucle d’exécution d’un thread peut être obtenue à partir du thread uniquement, la clé est donc ce temporisateur dans le thread.

À l’exception du runloop du thread principal, runloop devrait démarrer manuellement; il devrait y avoir quelques événements à gérer en exécutant runloop, comme Timer, sinon runloop se fermera, et nous pouvons l’utiliser pour quitter un runloop si timer est la seule source d’événement: invalider le timer.

Le code suivant est Swift 4:

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

Le temporisateur a une référence forte à la cible et runloop a une référence forte au temporisateur, après que le temporisateur soit invalidé, il libère la cible, gardez donc une référence faible dans la cible et l’invalidez au moment opportun pour quitter le runloop (puis quitter le thread).

Remarque: en tant qu’optimisation, la fonction de sync de DispatchQueue appelle le bloc sur le thread en cours lorsque cela est possible. En fait, vous exécutez le code ci-dessus dans le thread principal, la timer est lancée dans le thread principal, donc n’utilisez pas la fonction de sync , sinon la timer n’est pas déclenchée sur le thread souhaité.

Vous pouvez nommer le thread pour suivre son activité en interrompant l’exécution du programme dans Xcode. Dans GCD, utilisez:

 Thread.current.name = "ThreadWithTimer" 

Solution 1: Filetage

Nous pourrions utiliser NSThread directement. N’ayez pas peur, le code est facile.

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

Solution 2: Fil de sous-classe

Si vous souhaitez utiliser la sous-classe 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() } } 

Note: N’ajoutez pas de temporisateur dans init, sinon, le temporisateur est ajouté au runlop du thread de l’appelant d’initialisation, pas le runloop de ce thread, par exemple, vous exécutez le code suivant dans le thread principal. le thread run du thread, pas le runloop du timerThread. Vous pouvez le vérifier dans le timerMethod() .

 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 À propos de Runloop.current.run() , son document suggère de ne pas appeler cette méthode si nous voulons que runloop se termine, utilisez run(mode: RunLoopMode, before limitDate: Date) , run() cette méthode à plusieurs resockets dans NSDefaultRunloopMode , quel est le mode? Plus de détails dans runloop et thread .

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

Si vous souhaitez que votre NSTimer fonctionne en arrière-plan, procédez comme suit:

  1. appeler la méthode [self beginBackgroundTask] dans les méthodes applicationWillResignActive
  2. appeler la méthode [self endBackgroundTask] dans applicationWillEnterForeground

C’est tout

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