RunLoop — il ciclo di elaborazione degli eventi in iOS, implementato dagli oggetti CFRunLoop (Core Foundation) e NSRunLoop (Foundation). È un meccanismo che attende eventi (tocchi, timer, fonti di input, notifiche) e li distribuisce ai gestori corrispondenti sul thread. Ogni thread in iOS ha al massimo un RunLoop, ma viene creato automaticamente solo per il Main Thread. Secondo la Apple CFRunLoop Documentation, RunLoop è fondamentale per il funzionamento di timer, animazioni e monitoraggio delle fonti nei thread in background.
Punti chiave
RunLoop — un oggetto infrastrutturale di Core Foundation che organizza l'elaborazione degli eventi sul thread. In sostanza è un ciclo infinito (while true) che attende l'arrivo di eventi (sources) e li trasmette ai gestori. Quando non ci sono eventi, RunLoop mette il thread in modalità sleep, risparmiando energia della batteria. Quando arriva un evento, il thread si risveglia, lo elabora e si riaddormenta. RunLoop esiste solo in iOS/macOS (XNU + Core Foundation) — in Android il suo ruolo è svolto da Looper.
Ogni thread ha al massimo un RunLoop, che viene creato in modo lazy al primo accesso. Per il Main Thread, RunLoop viene creato automaticamente all'avvio dell'applicazione. Per i thread in background, RunLoop non viene creato finché non viene chiamato CFRunLoopGetCurrent() o RunLoop.current. Il RunLoop principale dell'applicazione è responsabile dell'elaborazione degli eventi touch, del rendering dello schermo, dell'esecuzione dei blocchi DispatchQueue.main e della gestione dei layer Core Animation.
RunLoop non è un thread — è un meccanismo all'interno di un thread. Un thread può esistere senza RunLoop (se esegue un'attività sincrona e termina), ma RunLoop non può esistere senza un thread. Quando un thread con RunLoop attivo non ha eventi, non blocca la CPU, ma rimane in attesa (waiting) — questa è la differenza fondamentale rispetto a un ciclo busy-wait che consuma il 100% della CPU.
RunLoop elabora due tipi di fonti di eventi: Input Sources (fonti di input) e Timer Sources (timer). Le Input Sources forniscono eventi asincroni: tocchi, movimenti del mouse, dati da socket, messaggi da altri thread (performSelector:onThread:). Le Timer Sources forniscono eventi sincroni in base a una pianificazione: NSTimer, CADisplayLink. Esistono anche gli Observers — punti di ingresso per monitorare lo stato di RunLoop.
Il ciclo RunLoop consiste in fasi sequenziali: ingresso nella modalità (kCFRunLoopEntry), elaborazione dei timer (kCFRunLoopBeforeTimers), elaborazione delle fonti di input (kCFRunLoopBeforeSources), elaborazione delle fonti (kCFRunLoopAfterWaiting), attesa (sleep), uscita dalla modalità (kCFRunLoopExit). Se nell'iterazione corrente non viene elaborato alcun evento, RunLoop mette il thread in sleep per un tempo indefinito fino al risveglio da un nuovo evento.
import Foundation
// Dimostrazione delle fasi di RunLoop tramite 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 attivato")
case .beforeTimers:
print("BeforeTimers — elaborazione timer")
case .beforeSources:
print("BeforeSources — elaborazione fonti")
case .afterWaiting:
print("AfterWaiting — risveglio dopo sleep")
case .exit:
print("Exit — RunLoop terminato")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Esempio: RunLoop elabora un timer sul thread principale
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// RunLoop.current.run() sul Main Thread viene chiamato da UIApplicationMain
// automaticamente — non è necessario avviarlo manualmente
RunLoop.current.run() // Questa chiamata non ritorna al Main Thread
}
L'esempio observeRunLoopActivities registra un Observer sul RunLoop principale che registra ogni fase del ciclo. Ciò è utile per il debug: se si nota un lungo intervallo tra .beforeTimers e .afterWaiting, significa che un'operazione sul Main Thread sta bloccando RunLoop. timerOnMainRunLoop mostra come NSTimer funziona automaticamente sul RunLoop principale — Timer.scheduledTimer aggiunge il timer al RunLoop corrente per impostazione predefinita (.default mode).
Android Looper — l'analogo di RunLoop. Looper.prepare() crea una coda di messaggi (MessageQueue) sul thread, Looper.loop() avvia il ciclo di elaborazione infinito. Handler invia messaggi e Runnable in questa coda. Differenza principale: RunLoop supporta le modalità (modes), mentre Android Looper no. Looper elabora tutti i messaggi senza filtraggio per modalità, rendendolo più semplice ma meno flessibile per scenari con priorità (ad esempio, lo scorrimento in iOS viene elaborato in modalità .tracking separatamente dagli altri eventi).
RunLoop Mode — un insieme di fonti, timer e osservatori attivi in un dato momento. Le modalità consentono di isolare l'elaborazione degli eventi per priorità. Quando l'utente scorre una UITableView, RunLoop passa alla modalità .tracking, in cui vengono elaborati solo gli eventi di scorrimento e i timer/animazioni corrispondenti. Tutte le altre fonti (ad esempio NSURLConnection) vengono sospese fino all'uscita dalla modalità di scorrimento.
Tre modalità principali: .default (NSDefaultRunLoopMode) — la modalità principale in cui vengono elaborati tutti gli eventi tranne lo scorrimento; .tracking (UITrackingRunLoopMode) — attivata durante lo scorrimento o la navigazione gestuale; .common (NSRunLoopCommonModes) — non una modalità separata ma un insieme di alias che include .default + .tracking. L'aggiunta di una fonte a .commonModes la aggiunge automaticamente a tutte le modalità dell'insieme.
| Modalità | Costante Core Foundation | Costante Foundation | Quando attiva |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Stato normale, senza scorrimento |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Scorrimento, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-modalità: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Primo avvio di RunLoop |
Problema classico: NSTimer, aggiunto in modalità .default, smette di attivarsi durante lo scorrimento perché RunLoop passa alla modalità .tracking e non elabora i timer di .default. Soluzione — aggiungere il timer a .commonModes: RunLoop.current.add(timer, forMode: .common). Ciò farà sì che il Timer si attivi sia in .default che in .tracking. Alternativa — utilizzare DispatchQueue.main.async invece di NSTimer, poiché GCD opera a livello di thread, non di modalità RunLoop.
I thread in background non hanno un RunLoop per impostazione predefinita. Se in un thread in background è necessario avviare NSTimer, elaborare NSInputStream/NSOutputStream o reagire a performSelector:, è necessario creare e avviare manualmente RunLoop. Senza RunLoop, il timer e performSelector: non si attiveranno mai — il thread eseguirà il codice e terminerà senza attendere eventi.
Per creare un RunLoop in un thread in background, è sufficiente chiamare RunLoop.current.run() alla fine del thread. Questa chiamata blocca il thread indefinitamente, elaborando gli eventi. Per fermarlo, utilizzare CFRunLoopStop(CFRunLoopGetCurrent()). Importante: RunLoop.current crea RunLoop in modo lazy al primo accesso — se run() non viene chiamato, non elaborerà gli eventi. Schema: configurazione delle fonti → aggiunta a RunLoop → chiamata di run().
import Foundation
// Thread in background con il proprio RunLoop
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop viene creato automaticamente alla chiamata di RunLoop.current
let runLoop = RunLoop.current
// Aggiunta di una porta per mantenere RunLoop attivo
runLoop.add(Port(), forMode: .default)
// Avvio dell'elaborazione degli eventi
var isFinished = false
while !isFinished {
// run(mode:before:) restituisce true se un evento è stato elaborato
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Arresto di RunLoop sul thread in background
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Utilizzo: timer sul RunLoop in background
let manager = BackgroundRunLoopManager()
manager.start()
// Invio di un'attività al RunLoop in background tramite performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager crea un thread in background con un RunLoop permanente. L'aggiunta di un Port() vuoto è necessaria per evitare che RunLoop termini immediatamente — senza fonti, RunLoop.run() restituisce false ed esce. performSelector:onThread: invia un messaggio al RunLoop in background — verrà elaborato quando RunLoop entra nella fase BeforeSources. Stop chiama CFRunLoopStop sul thread in background, terminando il ciclo.
NSTimer crea un evento timer che RunLoop elabora nella fase BeforeTimers. I timer possono essere repeating (ripetitivi) o non-repeating (una tantum). NSTimer non garantisce la precisione di attivazione: se RunLoop è bloccato da un'operazione lunga, il timer si attiverà dopo lo sblocco e tutte le attivazioni perse verranno unite in una sola (per un timer repeating — al massimo un'attivazione di recupero).
CADisplayLink — un timer specializzato, sincronizzato con la frequenza di aggiornamento dello schermo (60/120/144 Hz). Viene utilizzato per animazioni e aggiornamenti video. CADisplayLink viene aggiunto a RunLoop e si attiva prima di ogni frame di rendering (prima che Core Animation invii il layer per il rendering). Se un frame viene saltato (display link non è riuscito ad attivarsi entro 16 ms), la chiamata successiva avviene nel ciclo VSync successivo.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — animazione con vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Aggiunta alla modalità .common — funziona anche durante lo scorrimento
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Chiamato a ogni frame (60 FPS → ogni 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // arresto dopo 5 secondi
}
}
// NSTimer — attività periodica
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// CHIAVE: aggiungere al .common, altrimenti il timer si blocca durante lo scorrimento
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
In AnimationController, CADisplayLink viene aggiunto alla modalità .common, garantendo la chiamata di step a ogni frame indipendentemente dallo scorrimento. displayLink.add(to: .current, forMode: .common) — lo schema standard per animazioni che non devono essere interrotte durante lo scorrimento. Anche NSTimer viene aggiunto alla modalità .common per ticchettare durante lo scorrimento. Senza ciò, il timer si attiverebbe solo in modalità .default.
CFRunLoopObserver — un meccanismo per tracciare le fasi di RunLoop. Con Observer è possibile ricevere notifiche sull'ingresso in modalità, inizio elaborazione timer, inizio elaborazione fonti, risveglio dopo sleep, uscita dalla modalità. Gli Observers sono utilizzati dai framework per le proprie esigenze: Core Animation li usa per il rendering dei layer prima del sleep di RunLoop, UIKit per l'aggiornamento del layout dopo l'elaborazione degli eventi.
Lo sviluppatore può anche aggiungere Observers per i propri scopi. Ad esempio: misurazione del tempo di elaborazione degli eventi (profilazione), esecuzione di operazioni differite prima del sleep di RunLoop (quando l'interfaccia è già aggiornata e l'utente non interagisce), salvataggio automatico dei dati in caso di inattività prolungata. L'Observer viene registrato tramite CFRunLoopAddObserver con indicazione della modalità e della maschera di bit delle attività monitorate.
I punti più utili per Observer: .afterWaiting — eseguito dopo il risveglio di RunLoop e può contenere codice da lanciare dopo l'elaborazione di un evento; .beforeTimers — prima dell'elaborazione dei timer, consente di misurare il tempo trascorso dall'elaborazione precedente; .exit — si attiva all'arresto di RunLoop, utile per la pulizia delle risorse del thread in background.
CFRunLoopStop — funzione che termina forzatamente l'iterazione corrente di RunLoop. Alla chiamata di CFRunLoopStop(CFRunLoopGetCurrent()), RunLoop termina l'elaborazione dell'evento corrente ed esce da run() restituendo false. Questo è il metodo standard per arrestare RunLoop su un thread in background. Sul Main Thread, CFRunLoopStop non è raccomandato — il RunLoop principale deve funzionare per tutta la durata dell'applicazione. Per i thread in background, dopo CFRunLoopStop, il thread può terminare o continuare l'esecuzione del codice successivo a run().
Domande frequenti
RunLoop — il ciclo di elaborazione degli eventi in iOS, implementato da CFRunLoop (Core Foundation) e NSRunLoop (Foundation). Attende eventi (tocchi, timer, fonti di input) e li distribuisce ai gestori sul thread. Ogni thread può avere un RunLoop, ma automaticamente viene creato solo per il Main Thread. RunLoop gestisce le modalità (.default, .tracking, .common), isolando l'elaborazione per priorità.
NSTimer viene aggiunto per impostazione predefinita alla modalità .default di RunLoop. Quando l'utente scorre, RunLoop passa alla modalità .tracking e non elabora i timer di .default. Soluzione: aggiungere il timer alla modalità .common tramite RunLoop.current.add(timer, forMode: .common). .common unisce .default e .tracking, quindi il timer si attiva in entrambe le modalità.
Solo se il thread in background utilizza timer (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream o eventi Source. Se il thread esegue un'attività sincrona (download di file, calcoli) e termina — RunLoop non è necessario. Per avviarlo, chiamare RunLoop.current.run() dopo aver configurato le fonti. Per fermarlo — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop opera a livello di thread ed elabora gli eventi sequenzialmente con supporto delle modalità (modes). DispatchQueue — un'astrazione di pool di thread, le attività vengono eseguite su qualsiasi thread disponibile. GCD non supporta le modalità ed è indipendente da RunLoop. DispatchQueue.main utilizza il RunLoop principale per eseguire blocchi — questo è l'unico punto di intersezione. Per le attività in background, è preferibile GCD.
CADisplayLink — un timer sincronizzato con VSync (frequenza di aggiornamento dello schermo). Viene aggiunto a RunLoop e si attiva prima di ogni frame di rendering nella fase BeforeTimers. CADisplayLink funziona solo sul Main Thread, poiché il rendering dello schermo avviene lì. Per animazioni ininterrotte durante lo scorrimento, aggiungerlo in modalità .common: displayLink.add(to: .current, forMode: .common).
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche