RunLoop — la boucle de traitement des événements dans iOS, implémentée par les objets CFRunLoop (Core Foundation) et NSRunLoop (Foundation). C'est un mécanisme qui attend les événements (touchers, timers, sources d'entrée, notifications) et les distribue aux handlers correspondants sur le thread. Chaque thread dans iOS a au maximum un RunLoop, mais il n'est créé automatiquement que pour le Main Thread. Selon la Apple CFRunLoop Documentation, RunLoop est essentiel pour le fonctionnement des timers, des animations et de la surveillance des sources dans les threads d'arrière-plan.
L'essentiel
RunLoop — un objet d'infrastructure Core Foundation qui organise le traitement des événements sur le thread. Dans son principe, c'est une boucle infinie (while true) qui attend l'arrivée d'événements (sources) et les transmet aux handlers. Quand il n'y a pas d'événements, RunLoop met le thread en mode veille (sleep), économisant l'énergie de la batterie. Lorsqu'un événement arrive, le thread se réveille, le traite et se rendort. RunLoop n'existe que dans iOS/macOS (XNU + Core Foundation) — dans Android, son rôle est joué par Looper.
Chaque thread n'a pas plus d'un RunLoop, qui est créé paresseusement (lazy) lors du premier accès. Pour le Main Thread, RunLoop est créé automatiquement au démarrage de l'application. Pour les threads d'arrière-plan, RunLoop n'est pas créé tant que CFRunLoopGetCurrent() ou RunLoop.current n'est pas appelé. Le RunLoop principal de l'application est responsable du traitement des événements tactiles, du rendu d'écran, de l'exécution des blocs DispatchQueue.main et du service des couches Core Animation.
RunLoop n'est pas un thread — c'est un mécanisme à l'intérieur d'un thread. Un thread peut exister sans RunLoop (s'il exécute une tâche synchrone et se termine), mais RunLoop ne peut pas exister sans thread. Lorsqu'un thread avec un RunLoop actif n'a pas d'événements, il ne bloque pas le CPU, mais reste en attente (waiting) — c'est la différence clé avec une boucle busy-wait qui consomme 100% du CPU.
RunLoop traite deux types de sources d'événements : Input Sources (sources d'entrée) et Timer Sources (timers). Les Input Sources fournissent des événements asynchrones : touchers, mouvements de souris, données socket, messages d'autres threads (performSelector:onThread:). Les Timer Sources fournissent des événements synchrones selon un calendrier : NSTimer, CADisplayLink. Il existe également des Observers — des points d'entrée pour surveiller l'état de RunLoop.
Le cycle RunLoop se compose de phases séquentielles : entrée dans le mode (kCFRunLoopEntry), traitement des timers (kCFRunLoopBeforeTimers), traitement des sources d'entrée (kCFRunLoopBeforeSources), traitement des sources (kCFRunLoopAfterWaiting), attente (sleep), sortie du mode (kCFRunLoopExit). Si aucun événement n'est traité pendant l'itération actuelle, RunLoop met le thread en veille pour une durée indéterminée jusqu'à ce qu'un nouvel événement le réveille.
import Foundation
// Démonstration des phases de RunLoop via Observer
func observeRunLoopActivities() {
let observer = CFRunLoopObserverCreateWithHandler(
nil,
CFOptionFlags([[.entry, .beforeTimers, .beforeSources,
.afterWaiting, .exit]]),
true, // repeats
0 // priority
) { observer, activity in
switch activity {
case .entry:
print("Entry — RunLoop activé")
case .beforeTimers:
print("BeforeTimers — traitement des timers")
case .beforeSources:
print("BeforeSources — traitement des sources")
case .afterWaiting:
print("AfterWaiting — réveil après sleep")
case .exit:
print("Exit — RunLoop terminé")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Exemple : RunLoop traite un timer sur le thread principal
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// RunLoop.current.run() sur le Main Thread est appelé par UIApplicationMain
// automatiquement — pas besoin de démarrer manuellement
RunLoop.current.run() // Cet appel ne revient pas au Main Thread
}
L'exemple observeRunLoopActivities enregistre un Observer sur le RunLoop principal qui journalise chaque phase du cycle. Ceci est utile pour le débogage : si vous voyez un long intervalle entre .beforeTimers et .afterWaiting, cela signifie qu'une opération sur le Main Thread bloque RunLoop. timerOnMainRunLoop montre comment NSTimer fonctionne automatiquement sur le RunLoop principal — Timer.scheduledTimer ajoute le timer au RunLoop courant par défaut (.default mode).
Android Looper — l'équivalent de RunLoop. Looper.prepare() crée une file de messages (MessageQueue) sur le thread, Looper.loop() démarre la boucle de traitement infinie. Handler envoie des messages et des Runnable dans cette file. Différence principale : RunLoop prend en charge les modes (modes), contrairement à Android Looper. Looper traite tous les messages sans filtrage par mode, ce qui le rend plus simple mais moins flexible pour les scénarios avec priorités (par exemple, le défilement dans iOS est traité en mode .tracking séparément des autres événements).
RunLoop Mode — un ensemble de sources, timers et observateurs actifs à un moment donné. Les modes permettent d'isoler le traitement des événements par priorités. Lorsque l'utilisateur fait défiler un UITableView, RunLoop bascule en mode .tracking, où seuls les événements de défilement et les timers/animations correspondants sont traités. Toutes les autres sources (par exemple NSURLConnection) sont suspendues jusqu'à la sortie du mode de défilement.
Trois modes principaux : .default (NSDefaultRunLoopMode) — le mode principal où tous les événements sauf le défilement sont traités ; .tracking (UITrackingRunLoopMode) — activé lors du défilement ou de la navigation gestuelle ; .common (NSRunLoopCommonModes) — pas un mode séparé mais un ensemble d'alias qui inclut .default + .tracking. L'ajout d'une source à .commonModes l'ajoute automatiquement à tous les modes de l'ensemble.
| Mode | Constante Core Foundation | Constante Foundation | Quand actif |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | État normal, sans défilement |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Défilement, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-mode : default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Premier démarrage de RunLoop |
Problème classique : NSTimer, ajouté en mode .default, cesse de se déclencher pendant le défilement car RunLoop bascule en mode .tracking et ne traite pas les timers de .default. Solution — ajoutez le timer à .commonModes : RunLoop.current.add(timer, forMode: .common). Cela fera en sorte que le Timer se déclenche à la fois en .default et en .tracking. Alternative — utilisez DispatchQueue.main.async au lieu de NSTimer, car GCD fonctionne au niveau des threads, pas des modes RunLoop.
Les threads d'arrière-plan n'ont pas de RunLoop par défaut. Si vous devez démarrer un NSTimer, traiter NSInputStream/NSOutputStream ou réagir à performSelector: dans un thread d'arrière-plan, vous devez créer et démarrer manuellement RunLoop. Sans RunLoop, le timer et performSelector: ne se déclencheront jamais — le thread exécutera le code et se terminera sans attendre les événements.
Pour créer un RunLoop dans un thread d'arrière-plan, il suffit d'appeler RunLoop.current.run() à la fin du thread. Cet appel bloque le thread indéfiniment en traitant les événements. Pour l'arrêter, utilisez CFRunLoopStop(CFRunLoopGetCurrent()). Important : RunLoop.current crée RunLoop paresseusement lors du premier accès — si run() n'est pas appelé, il ne traitera pas les événements. Modèle : configuration des sources → ajout au RunLoop → appel de run().
import Foundation
// Thread d'arrière-plan avec son propre RunLoop
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop est créé automatiquement lors de l'appel à RunLoop.current
let runLoop = RunLoop.current
// Ajout d'un port pour maintenir RunLoop actif
runLoop.add(Port(), forMode: .default)
// Démarrage du traitement des événements
var isFinished = false
while !isFinished {
// run(mode:before:) retourne true si un événement a été traité
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Arrêt de RunLoop sur le thread d'arrière-plan
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Utilisation : timer sur le RunLoop d'arrière-plan
let manager = BackgroundRunLoopManager()
manager.start()
// Envoi d'une tâche au RunLoop d'arrière-plan via performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager crée un thread d'arrière-plan avec un RunLoop permanent. L'ajout d'un Port() vide est nécessaire pour que RunLoop ne se termine pas immédiatement — sans sources, RunLoop.run() retourne false et se termine. performSelector:onThread: envoie un message au RunLoop d'arrière-plan — il sera traité lorsque RunLoop entrera dans la phase BeforeSources. Stop appelle CFRunLoopStop sur le thread d'arrière-plan, terminant la boucle.
NSTimer crée un événement timer que RunLoop traite dans la phase BeforeTimers. Les timers peuvent être repeating (répétitifs) ou non-repeating (uniques). NSTimer ne garantit pas la précision de déclenchement : si RunLoop est bloqué par une opération longue, le timer se déclenchera après le déblocage, et tous les déclenchements manqués seront fusionnés en un seul (pour un timer repeating — au plus un déclenchement de rattrapage).
CADisplayLink — un timer spécialisé, synchronisé avec la fréquence de rafraîchissement de l'écran (60/120/144 Hz). Il est utilisé pour les animations et les mises à jour vidéo. CADisplayLink est ajouté au RunLoop et se déclenche avant chaque frame de rendu (avant que Core Animation n'envoie la couche pour rendu). Si une frame est sautée (display link n'a pas pu se déclencher en 16 ms), l'appel suivant se produit dans le cycle VSync suivant.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — animation avec vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Ajout au mode .common — fonctionne aussi pendant le défilement
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Appelé à chaque frame (60 FPS → toutes les 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // arrêt après 5 secondes
}
}
// NSTimer — tâche périodique
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// CLÉ : ajouter au .common, sinon le timer se fige au défilement
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
Dans AnimationController, CADisplayLink est ajouté au mode .common, ce qui garantit l'appel de step à chaque frame indépendamment du défilement. displayLink.add(to: .current, forMode: .common) — le modèle standard pour les animations qui ne doivent pas être interrompues lors du défilement. NSTimer est également ajouté au mode .common pour qu'il tictaque pendant le défilement. Sans cela, le timer ne se déclencherait qu'en mode .default.
CFRunLoopObserver — un mécanisme pour suivre les phases de RunLoop. Avec Observer, vous pouvez recevoir des notifications sur l'entrée en mode, le début du traitement des timers, le début du traitement des sources, le réveil après le sommeil, la sortie du mode. Les Observers sont utilisés par les frameworks pour leurs besoins : Core Animation les utilise pour le rendu des couches avant le sommeil de RunLoop, UIKit pour la mise à jour du layout après le traitement des événements.
Le développeur peut également ajouter des Observers pour ses propres besoins. Par exemple : mesure du temps de traitement des événements (profilage), exécution d'opérations différées avant le sommeil de RunLoop (quand l'interface est déjà mise à jour et que l'utilisateur n'interagit pas), sauvegarde automatique des données en cas d'inactivité prolongée. L'Observer est enregistré via CFRunLoopAddObserver avec indication du mode et du masque binaire des activités surveillées.
Les points les plus utiles pour Observer : .afterWaiting — exécuté après le réveil de RunLoop et peut contenir du code à lancer après le traitement d'un événement ; .beforeTimers — avant le traitement des timers, permet de mesurer le temps écoulé depuis le traitement précédent ; .exit — se déclenche à l'arrêt de RunLoop, utile pour le nettoyage des ressources du thread d'arrière-plan.
CFRunLoopStop — fonction qui termine de force l'itération courante de RunLoop. Lors de l'appel de CFRunLoopStop(CFRunLoopGetCurrent()), RunLoop termine le traitement de l'événement courant et sort de run() en retournant false. C'est la méthode standard pour arrêter RunLoop sur un thread d'arrière-plan. Sur le Main Thread, CFRunLoopStop n'est pas recommandé — le RunLoop principal doit fonctionner pendant toute la durée de vie de l'application. Pour les threads d'arrière-plan, après CFRunLoopStop, le thread peut se terminer ou continuer l'exécution du code suivant run().
Questions fréquentes
RunLoop — la boucle de traitement des événements dans iOS, implémentée par CFRunLoop (Core Foundation) et NSRunLoop (Foundation). Elle attend les événements (touchers, timers, sources d'entrée) et les distribue aux handlers sur le thread. Chaque thread peut avoir un RunLoop, mais automatiquement il n'est créé que pour le Main Thread. RunLoop gère les modes (.default, .tracking, .common), isolant le traitement par priorités.
NSTimer est ajouté par défaut au mode .default de RunLoop. Lorsque l'utilisateur fait défiler, RunLoop bascule en mode .tracking et ne traite pas les timers de .default. Solution : ajoutez le timer au mode .common via RunLoop.current.add(timer, forMode: .common). .common combine .default et .tracking, donc le timer se déclenche dans les deux modes.
Seulement si le thread d'arrière-plan utilise des timers (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream ou des événements Source. Si le thread exécute une tâche synchrone (téléchargement de fichier, calculs) et se termine — RunLoop n'est pas nécessaire. Pour démarrer, appelez RunLoop.current.run() après la configuration des sources. Pour arrêter — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop fonctionne au niveau du thread et traite les événements séquentiellement avec le support des modes. DispatchQueue — une abstraction de pool de threads, les tâches sont exécutées sur n'importe quel thread disponible. GCD ne supporte pas les modes et vit indépendamment de RunLoop. DispatchQueue.main utilise le RunLoop principal pour exécuter des blocs — c'est le seul point d'intersection. Pour les tâches d'arrière-plan, GCD est préférable.
CADisplayLink — un timer synchronisé avec VSync (fréquence de rafraîchissement de l'écran). Il est ajouté au RunLoop et se déclenche avant chaque frame de rendu dans la phase BeforeTimers. CADisplayLink ne fonctionne que sur le Main Thread, car le rendu d'écran s'y produit. Pour des animations ininterrompues lors du défilement, ajoutez-le en mode .common : displayLink.add(to: .current, forMode: .common).
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi