RunLoop in iOS: Was ist das, Arbeitsmodi und Ereignisschleife

Autor: IT Sectr Veröffentlicht: 2026-03-16 Lesezeit: 11 Min.

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 — eine Ereignisschleife, die Ereignisse im Thread verarbeitet: Berührungen, Timer, Eingabequellen
  • Main Thread hat einen automatischen RunLoop (CFRunLoopGetMain()), Hintergrundthreads müssen manuell gestartet werden
  • Drei Modi: .default (Standard), .tracking (Scrollen), .common (vereint default+tracking)
  • NSTimer und CADisplayLink funktionieren ohne aktiven RunLoop im Thread nicht
  • RunLoop observers ermöglichen Reaktion auf Ein-/Austritt aus Modi und Start/Ende der Verarbeitung

Was ist RunLoop

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.

Wie RunLoop funktioniert: Anatomie der Ereignisschleife

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.

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

RunLoop vs. Looper (Android)

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 Modi: default, tracking, common

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.

ModusCore Foundation KonstanteFoundation KonstanteWann aktiv
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultNormalzustand, ohne Scrollen
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingScrollen, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPseudo-Modus: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeErster Start des RunLoop

Warum NSTimer während des Scrollens nicht ausgelöst wird

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.

RunLoop in Hintergrundthreads

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.

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

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

RunLoop Observers: Überwachung der Schleifenereignisse

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 und Schleifenbeendigung

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

Was ist RunLoop in iOS?

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.

Warum funktioniert NSTimer während des Scrollens nicht?

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.

Muss RunLoop in Hintergrundthreads gestartet werden?

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

Was ist der Unterschied zwischen RunLoop und GCD DispatchQueue?

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.

Wie hängt CADisplayLink mit RunLoop zusammen?

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

  • RunLoop — Ereignisschleife von iOS: verarbeitet Berührungen, Timer, Eingabequellen im Thread; Main Thread hat automatischen RunLoop
  • Drei Modi: .default (allgemein), .tracking (Scrollen), .common (default + tracking) — steuern die Ereignisfilterung
  • NSTimer in .default wird beim Scrollen nicht ausgelöst — Lösung: Hinzufügen zum .common mode über RunLoop.current.add(timer, forMode: .common)
  • Hintergrundthreads haben standardmäßig keinen RunLoop — für Timer und performSelector: ist manueller Start über RunLoop.current.run() erforderlich
  • CADisplayLink — Timer für jeden VSync-Frame, unverzichtbar für flüssige Animationen; zum .common mode hinzufügen für Funktion beim Scrollen
  • RunLoop Observer — Phasenüberwachung: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; wird für Profiling verwendet
  • RunLoop ≠ Looper: iOS RunLoop unterstützt Modi und Timer, Android Looper ist einfacher — ohne Modi, Handler + MessageQueue

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.

Projekt besprechen

Lesen Sie auch