RunLoop — die Ereignisverarbeitungsschleife in iOS, implementiert durch CFRunLoop (Core Foundation) und NSRunLoop (Foundation). Dies ist ein Mechanismus, der auf Ereignisse wartet (Berührungen, Timer, Eingabequellen, Benachrichtigungen) und sie an die entsprechenden Handler im Thread weiterleitet. Jeder Thread in iOS hat maximal einen RunLoop, der jedoch nur für den Main Thread automatisch erstellt wird. Laut Apple CFRunLoop Documentation ist RunLoop für die Arbeit mit Timern, Animationen und die Überwachung von Quellen in Hintergrundthreads entscheidend.
Das Wichtigste
RunLoop — ein Core Foundation Infrastrukturobjekt, das die Ereignisverarbeitung im Thread organisiert. Im Wesentlichen ist es eine Endlosschleife (while true), die auf eingehende Ereignisse (sources) wartet und sie an die Handler übergibt. Wenn keine Ereignisse vorliegen, versetzt RunLoop den Thread in den Schlafmodus (sleep), um Akkuleistung zu sparen. Beim Eintreffen eines Ereignisses erwacht der Thread, verarbeitet es und schläft wieder ein. RunLoop existiert nur in iOS/macOS (XNU + Core Foundation) — in Android wird seine Rolle von Looper übernommen.
Jeder Thread hat höchstens einen RunLoop, der lazy beim ersten Zugriff erstellt wird. Für den Main Thread wird RunLoop automatisch beim Start der Anwendung erstellt. Für Hintergrundthreads wird RunLoop erst erstellt, wenn CFRunLoopGetCurrent() oder RunLoop.current aufgerufen wird. Der Haupt-RunLoop der Anwendung ist für die Verarbeitung von Touch-Ereignissen, Bildschirmdarstellung, Ausführung von DispatchQueue.main-Blöcken und die Bedienung von Core Animation Layern zuständig.
RunLoop ist kein Thread — es ist ein Mechanismus innerhalb eines Threads. Ein Thread kann ohne RunLoop existieren (wenn er eine synchrone Aufgabe ausführt und beendet wird), aber RunLoop kann nicht ohne Thread existieren. Wenn ein Thread mit aktivem RunLoop keine Ereignisse hat, blockiert er nicht die CPU, sondern befindet sich im Wartezustand (waiting) — dies ist der Hauptunterschied zu einer busy-wait-Schleife, die 100% CPU verbraucht.
RunLoop verarbeitet zwei Arten von Ereignisquellen: Input Sources (Eingabequellen) und Timer Sources (Timer). Input Sources liefern asynchrone Ereignisse: Berührungen, Mausbewegungen, Socket-Daten, Nachrichten von anderen Threads (performSelector:onThread:). Timer Sources liefern synchrone Ereignisse nach Zeitplan: NSTimer, CADisplayLink. Es gibt auch Observers — Einstiegspunkte zur Überwachung des RunLoop-Zustands.
Der RunLoop-Zyklus besteht aus aufeinanderfolgenden Phasen: Eintritt in den Modus (kCFRunLoopEntry), Verarbeitung von Timern (kCFRunLoopBeforeTimers), Verarbeitung von Eingabequellen (kCFRunLoopBeforeSources), Verarbeitung von Quellen (kCFRunLoopAfterWaiting), Warten (sleep), Austritt aus dem Modus (kCFRunLoopExit). Wenn in der aktuellen Iteration kein Ereignis verarbeitet wurde, versetzt RunLoop den Thread für unbestimmte Zeit in den Schlaf, bis ein neues Ereignis ihn weckt.
import Foundation
// Demonstration der RunLoop-Phasen durch 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 aktiviert")
case .beforeTimers:
print("BeforeTimers — Timer-Verarbeitung")
case .beforeSources:
print("BeforeSources — Quellenverarbeitung")
case .afterWaiting:
print("AfterWaiting — Aufwachen nach Sleep")
case .exit:
print("Exit — RunLoop beendet")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Beispiel: RunLoop verarbeitet Timer auf dem Hauptthread
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// RunLoop.current.run() auf dem Main Thread wird von UIApplicationMain aufgerufen
// automatisch — kein manueller Start erforderlich
RunLoop.current.run() // Dieser Aufruf kehrt nicht zum Main Thread zurück
}
Das Beispiel observeRunLoopActivities registriert einen Observer im Haupt-RunLoop, der jede Phase des Zyklus protokolliert. Dies ist nützlich für das Debugging: Wenn Sie eine lange Verzögerung zwischen .beforeTimers und .afterWaiting sehen, blockiert eine Operation auf dem Main Thread den RunLoop. timerOnMainRunLoop zeigt, wie NSTimer automatisch auf dem Haupt-RunLoop arbeitet — Timer.scheduledTimer fügt den Timer standardmäßig zum aktuellen RunLoop (.default mode) hinzu.
Android Looper — das Pendant zu RunLoop. Looper.prepare() erstellt eine Nachrichtenwarteschlange (MessageQueue) im Thread, Looper.loop() startet die Endlosverarbeitungsschleife. Handler sendet Nachrichten und Runnable in diese Warteschlange. Hauptunterschied: RunLoop unterstützt Modi (modes), Android Looper nicht. Looper verarbeitet alle Nachrichten ohne Filterung nach Modus, was einfacher, aber für prioritätsbasierte Szenarien weniger flexibel ist (z.B. wird Scrollen in iOS im .tracking mode getrennt von anderen Ereignissen verarbeitet).
RunLoop Mode — eine Menge von Quellen, Timern und Beobachtern, die aktuell aktiv sind. Modi ermöglichen die Isolierung der Ereignisverarbeitung nach Prioritäten. Wenn der Benutzer eine UITableView scrollt, wechselt RunLoop in den .tracking mode, in dem nur Scroll-Ereignisse und entsprechende Timer/Animationen verarbeitet werden. Alle anderen Quellen (z.B. NSURLConnection) werden bis zum Verlassen des Scroll-Modus pausiert.
Drei Hauptmodi: .default (NSDefaultRunLoopMode) — der Standardmodus, in dem alle Ereignisse außer Scrollen verarbeitet werden; .tracking (UITrackingRunLoopMode) — wird beim Scrollen oder bei Gestennavigation aktiviert; .common (NSRunLoopCommonModes) — kein separater Modus, sondern ein Alias-Set, das .default + .tracking umfasst. Das Hinzufügen einer Quelle zu .commonModes fügt sie automatisch zu allen Modi des Sets hinzu.
| Modus | Core Foundation Konstante | Foundation Konstante | Wann aktiv |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Normalzustand, ohne Scrollen |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Scrollen, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-Modus: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Erster Start des RunLoop |
Klassisches Problem: NSTimer, der im .default mode hinzugefügt wurde, wird während des Scrollens nicht ausgelöst, weil RunLoop in den .tracking mode wechselt und keine Timer aus .default verarbeitet. Lösung — fügen Sie den Timer zu .commonModes hinzu: RunLoop.current.add(timer, forMode: .common). Dadurch wird der Timer sowohl in .default als auch in .tracking ausgelöst. Alternative — verwenden Sie DispatchQueue.main.async statt NSTimer, da GCD auf Thread-Ebene arbeitet, nicht auf RunLoop-Modi.
Hintergrundthreads haben standardmäßig keinen RunLoop. Wenn in einem Hintergrundthread NSTimer gestartet, NSInputStream/NSOutputStream verarbeitet oder auf performSelector: reagiert werden muss, muss RunLoop manuell erstellt und gestartet werden. Ohne RunLoop werden Timer und performSelector: niemals ausgelöst — der Thread führt den Code aus und wird beendet, ohne auf Ereignisse zu warten.
Zum Erstellen eines RunLoop in einem Hintergrundthread reicht ein Aufruf von RunLoop.current.run() am Ende des Threads. Dieser Aufruf blockiert den Thread unendlich und verarbeitet Ereignisse. Zum Stoppen verwenden Sie CFRunLoopStop(CFRunLoopGetCurrent()). Wichtig: RunLoop.current erstellt RunLoop lazy beim ersten Zugriff — wenn run() nicht aufgerufen wird, werden keine Ereignisse verarbeitet. Muster: Quellen konfigurieren → zu RunLoop hinzufügen → run() aufrufen.
import Foundation
// Hintergrundthread mit eigenem RunLoop
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop wird automatisch beim Aufruf von RunLoop.current erstellt
let runLoop = RunLoop.current
// Fügen Sie einen Port hinzu, um den RunLoop aktiv zu halten
runLoop.add(Port(), forMode: .default)
// Starten der Ereignisverarbeitung
var isFinished = false
while !isFinished {
// run(mode:before:) gibt true zurück, wenn ein Ereignis verarbeitet wurde
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Stoppen des RunLoop im Hintergrundthread
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Verwendung: Timer im Hintergrund-RunLoop
let manager = BackgroundRunLoopManager()
manager.start()
// Senden einer Aufgabe an den Hintergrund-RunLoop über performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager erstellt einen Hintergrundthread mit permanentem RunLoop. Das Hinzufügen eines leeren Port() ist notwendig, damit RunLoop nicht sofort beendet wird — ohne Quellen gibt RunLoop.run() false zurück und wird beendet. performSelector:onThread: sendet eine Nachricht an den Hintergrund-RunLoop — sie wird verarbeitet, wenn RunLoop in die BeforeSources-Phase eintritt. Stop ruft CFRunLoopStop im Hintergrundthread auf und beendet die Schleife.
NSTimer erstellt ein Timer-Ereignis, das RunLoop in der BeforeTimers-Phase verarbeitet. Timer können repeating (wiederholt) oder non-repeating (einmalig) sein. NSTimer garantiert keine Auslösegenauigkeit: Wenn RunLoop durch eine lange Operation blockiert ist, wird der Timer nach der Entblockung ausgelöst, und alle verpassten Auslösungen werden zu einer zusammengefasst (bei repeating-Timern maximal eine „Nachhol“-Auslösung).
CADisplayLink — ein spezialisierter Timer, der mit der Bildwiederholrate des Bildschirms (60/120/144 Hz) synchronisiert ist. Er wird für Animationen und Video-Updates verwendet. CADisplayLink wird zum RunLoop hinzugefügt und vor jedem Render-Frame ausgelöst (bevor Core Animation die Ebene zum Rendern sendet). Wenn ein Frame übersprungen wird (display Link konnte innerhalb von 16 ms nicht ausgelöst werden), erfolgt der nächste Aufruf im nächsten VSync-Zyklus.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — Animation mit vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Hinzufügen zum .common mode — funktioniert auch beim Scrollen
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Wird in jedem Frame aufgerufen (60 FPS → alle 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // Stopp nach 5 Sekunden
}
}
// NSTimer — periodische Aufgabe
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// SCHLÜSSEL: zum .common hinzufügen, sonst friert der Timer beim Scrollen ein
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
Im AnimationController wird CADisplayLink zum .common mode hinzugefügt, was den step-Aufruf in jedem Frame unabhängig vom Scrollen garantiert. displayLink.add(to: .current, forMode: .common) — das Standardmuster für Animationen, die beim Scrollen nicht unterbrochen werden dürfen. NSTimer wird ebenfalls zum .common mode hinzugefügt, um während des Scrollens zu ticken. Ohne dies würde der Timer nur im .default mode ausgelöst.
CFRunLoopObserver — ein Mechanismus zur Verfolgung von RunLoop-Phasen. Mit Observer können Benachrichtigungen über Moduseintritt, Start der Timer-Verarbeitung, Start der Quellenverarbeitung, Aufwachen nach Schlaf und Modusaustritt empfangen werden. Observer werden von Frameworks für eigene Zwecke verwendet: Core Animation nutzt sie zum Rendern von Layern vor dem RunLoop-Schlaf, UIKit zum Aktualisieren des Layouts nach der Ereignisverarbeitung.
Entwickler können Observer auch für eigene Zwecke hinzufügen. Zum Beispiel: Messung der Ereignisverarbeitungszeit (Profiling), Ausführung verzögerter Operationen vor dem RunLoop-Schlaf (wenn die UI bereits aktualisiert ist und der Benutzer nicht interagiert), automatisches Speichern von Daten bei längerer Inaktivität. Observer wird über CFRunLoopAddObserver mit Angabe des Modus und einer Bitmaske der zu überwachenden Aktivitäten registriert.
Die nützlichsten Punkte für Observer: .afterWaiting — wird nach dem Aufwachen des RunLoop ausgeführt und kann Code enthalten, der nach der Ereignisverarbeitung laufen soll; .beforeTimers — vor der Timer-Verarbeitung, ermöglicht Messung der seit der letzten Verarbeitung vergangenen Zeit; .exit — wird beim Stoppen des RunLoop ausgelöst, nützlich für die Bereinigung von Ressourcen des Hintergrundthreads.
CFRunLoopStop — Funktion, die die aktuelle RunLoop-Iteration erzwingend beendet. Bei Aufruf von CFRunLoopStop(CFRunLoopGetCurrent()) beendet RunLoop die Verarbeitung des aktuellen Ereignisses und verlässt run() mit Rückgabe von false. Dies ist die Standardmethode zum Stoppen von RunLoop in Hintergrundthreads. Auf dem Main Thread wird CFRunLoopStop nicht empfohlen — der Haupt-RunLoop sollte während der gesamten Lebensdauer der Anwendung laufen. Bei Hintergrundthreads kann der Thread nach CFRunLoopStop beendet werden oder mit der Ausführung des Codes nach run() fortfahren.
Häufig gestellte Fragen
RunLoop — die Ereignisverarbeitungsschleife in iOS, implementiert durch CFRunLoop (Core Foundation) und NSRunLoop (Foundation). Sie wartet auf Ereignisse (Berührungen, Timer, Eingabequellen) und leitet sie an die Handler im Thread weiter. Jeder Thread kann einen RunLoop haben, aber automatisch wird er nur für den Main Thread erstellt. RunLoop verwaltet Modi (.default, .tracking, .common) und isoliert die Verarbeitung nach Prioritäten.
NSTimer wird standardmäßig zum .default RunLoop-Modus hinzugefügt. Wenn der Benutzer scrollt, wechselt RunLoop in den .tracking mode und verarbeitet keine Timer aus .default. Lösung: Fügen Sie den Timer zum .common mode hinzu über RunLoop.current.add(timer, forMode: .common). .common vereint .default und .tracking, sodass der Timer in beiden Modi ausgelöst wird.
Nur wenn der Hintergrundthread Timer (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream oder Source-Ereignisse verwendet. Wenn der Thread eine synchrone Aufgabe ausführt (Dateidownload, Berechnungen) und beendet wird — ist RunLoop nicht erforderlich. Zum Starten rufen Sie RunLoop.current.run() nach der Konfiguration der Quellen auf. Zum Stoppen — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop arbeitet auf Thread-Ebene und verarbeitet Ereignisse sequenziell mit Unterstützung von Modi (modes). DispatchQueue — eine Abstraktion eines Thread-Pools, Aufgaben werden auf jedem verfügbaren Thread ausgeführt. GCD unterstützt keine Modi und ist unabhängig von RunLoop. DispatchQueue.main verwendet den Haupt-RunLoop zur Ausführung von Blöcken — dies ist der einzige Berührungspunkt. Für Hintergrundaufgaben ist GCD vorzuziehen.
CADisplayLink — ein Timer, der mit VSync (Bildwiederholrate) synchronisiert ist. Er wird zum RunLoop hinzugefügt und vor jedem Render-Frame in der BeforeTimers-Phase ausgelöst. CADisplayLink funktioniert nur auf dem Main Thread, da die Bildschirmdarstellung dort erfolgt. Für unterbrechungsfreie Animationen beim Scrollen fügen Sie ihn zum .common mode hinzu: displayLink.add(to: .current, forMode: .common).
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch