RunLoop — de gebeurtenislus in iOS, geïmplementeerd door CFRunLoop (Core Foundation) en NSRunLoop (Foundation). Dit is een mechanisme dat wacht op gebeurtenissen (aanrakingen, timers, invoerbronnen, meldingen) en deze doorstuurt naar de juiste handlers op de thread. Elke thread in iOS heeft maximaal één RunLoop, maar deze wordt alleen automatisch aangemaakt voor de Main Thread. Volgens Apple CFRunLoop Documentatie is RunLoop cruciaal voor het werken van timers, animaties en het monitoren van bronnen in achtergrondthreads.
Belangrijkste punten
RunLoop — is een infrastructuurobject van Core Foundation dat de verwerking van gebeurtenissen op de thread organiseert. In essentie is het een oneindige lus (while true) die wacht op het binnenkomen van gebeurtenissen (sources) en deze doorgeeft aan handlers. Wanneer er geen gebeurtenissen zijn, zet RunLoop de thread in slaapstand (sleep), wat batterij energie bespaart. Bij het binnenkomen van een gebeurtenis wordt de thread gewekt, verwerkt deze en gaat weer slapen. RunLoop bestaat alleen in iOS/macOS (XNU + Core Foundation) — in Android wordt zijn rol vervuld door Looper.
Elke thread heeft maximaal één RunLoop, die lui (lazy) wordt aangemaakt bij de eerste toegang. Voor de Main Thread wordt RunLoop automatisch aangemaakt bij het starten van de app. Voor achtergrondthreads wordt RunLoop niet aangemaakt totdat CFRunLoopGetCurrent() of RunLoop.current wordt aangeroepen. De hoofd-RunLoop van de app is verantwoordelijk voor het verwerken van aanraakgebeurtenissen, schermweergave, het uitvoeren van DispatchQueue.main-blokken en het bedienen van Core Animation-lagen.
RunLoop is geen thread — het is een mechanisme binnen de thread. Een thread kan bestaan zonder RunLoop (als deze een synchrone taak uitvoert en beëindigt), maar RunLoop kan niet bestaan zonder thread. Wanneer een thread met actieve RunLoop geen gebeurtenissen heeft, blokkeert deze de CPU niet, maar bevindt zich in wachtstand (waiting) — dit is het belangrijkste verschil met busy-wait, die 100% CPU verbruikt.
RunLoop verwerkt twee soorten gebeurtenisbronnen: Input Sources (invoerbronnen) en Timer Sources (timers). Input Sources leveren asynchrone gebeurtenissen: aanrakingen, muisbewegingen, gegevens van sockets, berichten van andere threads (performSelector:onThread:). Timer Sources leveren synchrone gebeurtenissen volgens schema: NSTimer, CADisplayLink. Er bestaan ook Observers — ingangspunten voor het monitoren van de RunLoop-status.
De RunLoop-cyclus bestaat uit opeenvolgende fasen: ingang in modus (kCFRunLoopEntry), verwerking van timers (kCFRunLoopBeforeTimers), verwerking van invoerbronnen (kCFRunLoopBeforeSources), verwerking van bronnen (kCFRunLoopAfterWaiting), wachten (sleep), uitgang uit modus (kCFRunLoopExit). Als in de huidige iteratie geen gebeurtenis is verwerkt, zet RunLoop de thread voor onbepaalde tijd in slaap tot het ontwaken door een nieuwe gebeurtenis.
import Foundation
// Demonstratie van RunLoop-fasen 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 geactiveerd")
case .beforeTimers:
print("BeforeTimers — verwerking van timers")
case .beforeSources:
print("BeforeSources — verwerking van bronnen")
case .afterWaiting:
print("AfterWaiting — ontwaken na slaap")
case .exit:
print("Exit — RunLoop beëindigd")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Voorbeeld: RunLoop verwerkt een timer op de hoofdthread
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// RunLoop.current.run() op Main Thread wordt aangeroepen door UIApplicationMain
// automatisch — niet handmatig starten
RunLoop.current.run() // Deze aanroep keert niet terug naar Main Thread
}
Het voorbeeld observeRunLoopActivities registreert een Observer op de hoofd-RunLoop die elke fase van de cyclus logt. Dit is nuttig voor debugging: als u een lange interval ziet tussen .beforeTimers en .afterWaiting, betekent dit dat RunLoop is geblokkeerd door een bewerking op de Main Thread. timerOnMainRunLoop toont hoe NSTimer automatisch werkt op de hoofd-RunLoop — bij het aanmaken van Timer.scheduledTimer wordt de timer standaard aan de huidige RunLoop toegevoegd (.default mode).
Android Looper — de analogue van RunLoop. Looper.prepare() creëert een berichtenwachtrij (MessageQueue) op de thread, Looper.loop() start een oneindige verwerkingslus. Handler stuurt berichten en Runnable naar deze wachtrij. Het belangrijkste verschil: RunLoop ondersteunt modi (modes), Android Looper niet. Looper verwerkt alle berichten zonder filtering op modus, wat het eenvoudiger maar minder flexibel maakt in scenario's met prioriteiten (bijv. scrollen in iOS wordt in .tracking modus apart van andere gebeurtenissen verwerkt).
RunLoop Mode — is een set bronnen, timers en observers die op een bepaald moment actief zijn. Modi maken het mogelijk om gebeurtenisverwerking te isoleren op basis van prioriteiten. Wanneer de gebruiker door een UITableView scrollt, schakelt RunLoop naar .tracking modus, waarin alleen scrollgebeurtenissen en bijbehorende timers/animaties worden verwerkt. Alle andere bronnen (bijv. NSURLConnection) worden onderbroken tot het verlaten van de scrollmodus.
Drie hoofdmodi: .default (NSDefaultRunLoopMode) — de hoofdmodus waarin alle gebeurtenissen behalve scrollen worden verwerkt; .tracking (UITrackingRunLoopMode) — wordt geactiveerd bij scrollen of gebarennavigatie; .common (NSRunLoopCommonModes) — is geen aparte modus, maar een set aliassen die .default + .tracking omvat. Het toevoegen van een bron aan .commonModes voegt deze automatisch toe aan alle modi van de set.
| Modus | Core Foundation constante | Foundation constante | Wanneer actief |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Normale toestand, zonder scrollen |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Scrollen, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-modus: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Eerste start van RunLoop |
Klassiek probleem: NSTimer toegevoegd in .default modus stopt met werken tijdens scrollen, omdat RunLoop overschakelt naar .tracking modus en geen timers uit .default verwerkt. Oplossing — voeg de timer toe aan .commonModes: RunLoop.current.add(timer, forMode: .common). Dit zorgt ervoor dat de timer werkt in zowel .default als .tracking. Een alternatief is het gebruik van DispatchQueue.main.async in plaats van NSTimer, omdat GCD werkt op threadniveau, niet op RunLoop-modusniveau.
Achtergrondthreads hebben standaard geen RunLoop. Als in een achtergrondthread NSTimer moet worden gestart, NSInputStream/NSOutputStream moet worden verwerkt of op performSelector: moet worden gereageerd, moet u handmatig een RunLoop aanmaken en starten. Zonder RunLoop zullen de timer en performSelector: nooit werken — de thread voert de code uit en beëindigt zonder op gebeurtenissen te wachten.
Om een RunLoop in een achtergrondthread te creëren, volstaat het om aan het einde van de thread RunLoop.current.run() aan te roepen. Deze aanroep blokkeert de thread voor onbepaalde tijd en verwerkt gebeurtenissen. Gebruik CFRunLoopStop(CFRunLoopGetCurrent()) om te stoppen. Belangrijk: RunLoop.current maakt lui een RunLoop aan bij de eerste toegang — als u run() niet aanroept, worden er geen gebeurtenissen verwerkt. Patroon: configuratie van bronnen -> toevoegen aan RunLoop -> aanroep van run().
import Foundation
// Achtergrondthread met eigen RunLoop
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop wordt automatisch aangemaakt bij aanroep van RunLoop.current
let runLoop = RunLoop.current
// Voegen een poort toe om RunLoop actief te houden
runLoop.add(Port(), forMode: .default)
// Starten verwerking van gebeurtenissen
var isFinished = false
while !isFinished {
// run(mode:before:) geeft true terug als een gebeurtenis is verwerkt
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Stoppen van RunLoop op achtergrondthread
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Gebruik: timer op achtergrond-RunLoop
let manager = BackgroundRunLoopManager()
manager.start()
// Verzenden van taak naar achtergrond-RunLoop via performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager creëert een achtergrondthread met permanente RunLoop. Het toevoegen van een lege Port() is nodig om te voorkomen dat RunLoop onmiddellijk eindigt — zonder bronnen geeft RunLoop.run() false terug en stopt. performSelector:onThread: stuurt een bericht naar de achtergrond-RunLoop — het wordt verwerkt wanneer RunLoop de BeforeSources-fase binnengaat. Stop roept CFRunLoopStop aan op de achtergrondthread en beëindigt de cyclus.
NSTimer creëert een timer-gebeurtenis die RunLoop verwerkt in de BeforeTimers-fase. Timers kunnen repeating (herhalend) en non-repeating (eenmalig) zijn. NSTimer garandeert geen nauwkeurigheid: als RunLoop is geblokkeerd door een lange bewerking, zal de timer worden geactiveerd na deblokkering en worden alle gemiste activeringen samengevoegd tot één (voor herhalende timer — maximaal één „ingehaalde“ activering).
CADisplayLink — een gespecialiseerde timer gesynchroniseerd met de verversingsfrequentie van het scherm (60/120/144 Hz). Het wordt gebruikt voor animaties en video-updates. CADisplayLink wordt toegevoegd aan RunLoop en activeert vóór elk weergaveframe (voordat Core Animation de laag naar weergave stuurt). Als een frame wordt overgeslagen (display link niet binnen 16 ms kon activeren), vindt de volgende aanroep plaats in de volgende VSync-cyclus.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — animatie met vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Toevoegen in .common modus — werkt ook tijdens scrollen
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Wordt elk frame aangeroepen (60 FPS → elke 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // stoppen na 5 seconden
}
}
// NSTimer — periodieke taak
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// SLEUTEL: toevoegen in .common, anders bevriest de timer bij scrollen
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
In AnimationController is CADisplayLink toegevoegd in .common modus, wat garandeert dat step op elk frame wordt aangeroepen, ongeacht scrollen. displayLink.add(to: .current, forMode: .common) — het standaardpatroon voor animaties die niet onderbroken mogen worden tijdens scrollen. NSTimer is ook toegevoegd in .common modus om te tikken tijdens scrollen. Zonder dit zou de timer alleen in .default modus werken.
CFRunLoopObserver — mechanisme voor het volgen van RunLoop-fasen. Met Observer kunt u meldingen ontvangen over het betreden van een modus, het starten van timerverwerking, het starten van bronverwerking, ontwaken uit slaap, verlaten van een modus. Observers worden door frameworks gebruikt voor hun eigen behoeften: Core Animation gebruikt ze voor het weergeven van lagen vóór RunLoop-slaap, UIKit — voor het bijwerken van layout na verwerking van gebeurtenissen.
De ontwikkelaar kan ook Observers toevoegen voor eigen doeleinden. Bijvoorbeeld: meten van verwerkingstijd van gebeurtenissen (profilering), uitvoeren van uitgestelde bewerkingen vóór RunLoop-slaap (wanneer UI al is bijgewerkt en de gebruiker geen interactie heeft), automatisch opslaan van gegevens bij lange inactiviteit. Observer wordt geregistreerd via CFRunLoopAddObserver met opgave van modus en bitmasker van gevolgde activiteiten.
Meest nuttige punten voor Observer: .afterWaiting — wordt uitgevoerd na het ontwaken van RunLoop en kan code bevatten die moet worden gestart na verwerking van een gebeurtenis; .beforeTimers — vóór timerverwerking, maakt het meten van de tijd sinds de vorige verwerking mogelijk; .exit — wordt geactiveerd bij stoppen van RunLoop, nuttig voor opschonen van bronnen van de achtergrondthread.
CFRunLoopStop — functie die de huidige RunLoop-iteratie geforceerd beëindigt. Bij aanroep van CFRunLoopStop(CFRunLoopGetCurrent()) beëindigt RunLoop de verwerking van de huidige gebeurtenis en verlaat run() met false als retourwaarde. Dit is de standaardmanier om RunLoop op een achtergrondthread te stoppen. Op de Main Thread wordt CFRunLoopStop niet aanbevolen — de hoofd-RunLoop moet gedurende de hele levensduur van de app werken. Voor achtergrondthreads kan de thread na CFRunLoopStop beëindigen of doorgaan met het uitvoeren van volgende code na run().
Veelgestelde vragen
RunLoop — de gebeurtenislus in iOS geïmplementeerd door CFRunLoop (Core Foundation) en NSRunLoop (Foundation). Het wacht op gebeurtenissen (aanrakingen, timers, invoerbronnen) en stuurt ze door naar handlers op de thread. Elke thread kan één RunLoop hebben, maar automatisch wordt deze alleen voor de Main Thread aangemaakt. RunLoop beheert modi (.default, .tracking, .common) en isoleert verwerking op basis van prioriteiten.
NSTimer wordt standaard toegevoegd in .default modus van RunLoop. Wanneer de gebruiker scrollt, schakelt RunLoop naar .tracking modus en verwerkt geen timers uit .default. Oplossing: voeg de timer toe in .common modus via RunLoop.current.add(timer, forMode: .common). .common combineert .default en .tracking, dus de timer werkt in beide modi.
Alleen als de achtergrondthread timers (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream of Source-gebeurtenissen gebruikt. Als de thread een synchrone taak (bestand downloaden, berekeningen) uitvoert en beëindigt — is RunLoop niet nodig. Start door RunLoop.current.run() aan te roepen na configuratie van bronnen. Stop door — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop werkt op threadniveau en verwerkt gebeurtenissen sequentieel, met ondersteuning voor modi (modes). DispatchQueue — een abstractie van een threadpool, taken worden op elke vrije thread uitgevoerd. GCD ondersteunt geen modi en bestaat onafhankelijk van RunLoop. DispatchQueue.main gebruikt de hoofd-RunLoop voor het uitvoeren van blokken — dit is het enige snijpunt. Voor achtergrondtaken heeft GCD de voorkeur.
CADisplayLink — een timer gesynchroniseerd met VSync (schermverversingsfrequentie). Het wordt toegevoegd aan RunLoop en activeert vóór elk weergaveframe in de BeforeTimers-fase. CADisplayLink werkt alleen op de Main Thread, omdat schermweergave daar plaatsvindt. Voor continue animaties tijdens scrollen voegt u het toe in .common modus: displayLink.add(to: .current, forMode: .common).
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook