RunLoop dans iOS : qu'est-ce que c'est, modes de fonctionnement et boucle d'événements

Auteur : IT Sectr Publié le : 2026-03-16 Temps de lecture : 11 min

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 — boucle d'événements qui traite les événements sur le thread : touchers, timers, sources d'entrée
  • Main Thread dispose d'un RunLoop automatique (CFRunLoopGetMain()), les threads d'arrière-plan doivent être démarrés manuellement
  • Trois modes : .default (principal), .tracking (défilement), .common (combine default+tracking)
  • NSTimer et CADisplayLink ne fonctionnent pas sans RunLoop actif sur le thread
  • RunLoop observers permettent de réagir à l'entrée/sortie des modes et au début/fin du traitement

Qu'est-ce que RunLoop

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.

Comment fonctionne RunLoop : anatomie de la boucle d'événements

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.

swift
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).

RunLoop vs Looper (Android)

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

Modes RunLoop : default, tracking, common

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.

ModeConstante Core FoundationConstante FoundationQuand actif
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultÉtat normal, sans défilement
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingDéfilement, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPseudo-mode : default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModePremier démarrage de RunLoop

Pourquoi NSTimer ne se déclenche pas pendant le défilement

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.

RunLoop dans les threads d'arrière-plan

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

swift
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.

swift
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.

RunLoop Observers : surveillance des événements de la boucle

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 et fin de la boucle

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

Qu'est-ce que RunLoop dans iOS ?

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.

Pourquoi NSTimer ne fonctionne-t-il pas pendant le défilement ?

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.

Faut-il démarrer RunLoop dans un thread d'arrière-plan ?

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

Quelle est la différence entre RunLoop et GCD DispatchQueue ?

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.

Comment CADisplayLink est-il lié à RunLoop ?

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é

  • RunLoop — boucle d'événements iOS : traite les touchers, timers, sources d'entrée sur le thread ; le Main Thread a un RunLoop automatique
  • Trois modes : .default (général), .tracking (défilement), .common (default + tracking) — contrôlent le filtrage des événements
  • NSTimer en .default ne se déclenche pas pendant le défilement — solution : ajout au mode .common via RunLoop.current.add(timer, forMode: .common)
  • Threads d'arrière-plan n'ont pas de RunLoop par défaut — pour timers et performSelector: un démarrage manuel via RunLoop.current.run() est requis
  • CADisplayLink — timer pour chaque frame VSync, indispensable pour des animations fluides ; à ajouter en .common pour fonctionner pendant le défilement
  • RunLoop Observer — surveillance des phases : Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit ; utilisé pour le profilage
  • RunLoop ≠ Looper : RunLoop iOS supporte les modes et les timers, Looper Android est plus simple — sans modes, Handler + MessageQueue

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.

Discuter du projet

Lisez aussi