RunLoop v iOS: co to je, pracovní režimy a cyklus událostí

Autor: IT Sectr Publikováno: 2026-03-16 Doba čtení: 11 min

RunLoop — cyklus zpracování událostí v iOS, implementovaný objekty CFRunLoop (Core Foundation) a NSRunLoop (Foundation). Jedná se o mechanismus, který čeká na události (dotyky, časovače, zdroje vstupu, oznámení) a předává je příslušným obsluhám ve vlákně. Každé vlákno v iOS má maximálně jeden RunLoop, ale je automaticky vytvářen pouze pro Main Thread. Podle dokumentace Apple CFRunLoop je RunLoop kritický pro práci časovačů, animací a monitorování zdrojů ve vláknech na pozadí.

Nejdůležitější

  • RunLoop — event loop, který zpracovává události ve vlákně: dotyky, časovače, zdroje vstupu
  • Main Thread má automatický RunLoop (CFRunLoopGetMain()), vlákna na pozadí je třeba spustit ručně
  • Tři režimy: .default (hlavní), .tracking (posouvání), .common (spojuje default+tracking)
  • NSTimer a CADisplayLink nefungují bez aktivního RunLoop ve vlákně
  • RunLoop observers umožňují reagovat na vstup/výstup z režimů a začátek/konec zpracování

Co je RunLoop

RunLoop — infrastrukturní objekt Core Foundation, který organizuje zpracování událostí ve vlákně. V podstatě se jedná o nekonečnou smyčku (while true), která čeká na příchod událostí (sources) a předává je obsluhám. Když nejsou žádné události, RunLoop uvede vlákno do režimu spánku (sleep), šetří energii baterie. Při příchodu události se vlákno probudí, zpracuje ji a znovu usne. RunLoop existuje pouze v iOS/macOS (XNU + Core Foundation) — v Androidu jeho roli plní Looper.

Každé vlákno má nejvýše jeden RunLoop, který je vytvářen líně (lazy) při prvním přístupu. Pro Main Thread je RunLoop vytvářen automaticky při spuštění aplikace. Pro vlákna na pozadí není RunLoop vytvářen, dokud není zavoláno CFRunLoopGetCurrent() nebo RunLoop.current. Hlavní RunLoop aplikace je zodpovědný za zpracování dotykových událostí, vykreslování obrazovky, provádění bloků DispatchQueue.main a obsluhu vrstev Core Animation.

RunLoop není vlákno — jedná se o mechanismus uvnitř vlákna. Vlákno může existovat bez RunLoop (pokud provádí synchronní úkol a končí), ale RunLoop nemůže existovat bez vlákna. Když vlákno s aktivním RunLoop nemá žádné události, neblokuje CPU, ale nachází se ve stavu čekání (waiting) — to je klíčový rozdíl oproti busy-wait, který spotřebovává 100% CPU.

Jak funguje RunLoop: anatomie cyklu událostí

RunLoop zpracovává dva typy zdrojů událostí: Input Sources (vstupní zdroje) a Timer Sources (časovače). Input Sources doručují asynchronní události: dotyky, pohyby myši, data ze socketu, zprávy z jiných vláken (performSelector:onThread:). Timer Sources doručují synchronní události podle plánu: NSTimer, CADisplayLink. Existují také Observers — vstupní body pro sledování stavu RunLoop.

Cyklus RunLoop se skládá z následných fází: vstup do režimu (kCFRunLoopEntry), zpracování časovačů (kCFRunLoopBeforeTimers), zpracování vstupních zdrojů (kCFRunLoopBeforeSources), zpracování zdrojů (kCFRunLoopAfterWaiting), čekání (sleep), výstup z režimu (kCFRunLoopExit). Pokud v aktuální iteraci nebyla zpracována žádná událost, RunLoop uvede vlákno do spánku na neurčitou dobu až do probuzení novou událostí.

swift
import Foundation

// Demonstrace fází RunLoop prostřednictvím 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 aktivován")
        case .beforeTimers:
            print("BeforeTimers — zpracování časovačů")
        case .beforeSources:
            print("BeforeSources — zpracování zdrojů")
        case .afterWaiting:
            print("AfterWaiting — probuzení po spánku")
        case .exit:
            print("Exit — RunLoop dokončen")
        default:
            break
        }
    }

    CFRunLoopAddObserver(
        CFRunLoopGetCurrent(),
        observer,
        .commonModes
    )
}

// Příklad: RunLoop zpracovává časovač na hlavním vlákně
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // RunLoop.current.run() na Main Thread je voláno UIApplicationMain
    // automaticky — není třeba spouštět ručně
    RunLoop.current.run() // Toto volání se nevrátí na Main Thread
}

Příklad observeRunLoopActivities registruje Observer na hlavním RunLoop, který protokoluje každou fázi cyklu. To je užitečné pro ladění: pokud vidíte dlouhý interval mezi .beforeTimers a .afterWaiting, znamená to, že RunLoop je blokován operací na Main Thread. timerOnMainRunLoop ukazuje, jak NSTimer automaticky funguje na hlavním RunLoop — při vytvoření Timer.scheduledTimer je časovač přidán do aktuálního RunLoop ve výchozím nastavení (.default mode).

RunLoop vs Looper (Android)

Android Looper — obdoba RunLoop. Looper.prepare() vytváří frontu zpráv (MessageQueue) ve vlákně, Looper.loop() spouští nekonečnou smyčku zpracování. Handler posílá zprávy a Runnable do této fronty. Hlavní rozdíl: RunLoop podporuje režimy (modes), zatímco Android Looper ne. Looper zpracovává všechny zprávy bez filtrování podle režimu, což ho činí jednodušším, ale méně flexibilním ve scénářích s prioritami (např. posouvání v iOS je zpracováváno v režimu .tracking odděleně od ostatních událostí).

Režimy RunLoop: default, tracking, common

RunLoop Mode — je sada zdrojů, časovačů a pozorovatelů, které jsou aktuálně aktivní. Režimy umožňují izolovat zpracování událostí podle priorit. Když uživatel posouvá UITableView, RunLoop přepne do režimu .tracking, ve kterém jsou zpracovávány pouze události posouvání a odpovídající časovače/animace. Všechny ostatní zdroje (např. NSURLConnection) jsou pozastaveny až do opuštění režimu posouvání.

Tři hlavní režimy: .default (NSDefaultRunLoopMode) — hlavní režim, ve kterém jsou zpracovávány všechny události kromě posouvání; .tracking (UITrackingRunLoopMode) — aktivuje se při posouvání nebo gestech; .common (NSRunLoopCommonModes) — není samostatný režim, ale sada aliasů zahrnující .default + .tracking. Přidání zdroje do .commonModes jej automaticky přidá do všech režimů sady.

RežimKonstanta Core FoundationKonstanta FoundationKdy je aktivní
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultNormální stav, bez posouvání
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingPosouvání, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPseudo-režim: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModePrvní spuštění RunLoop

Proč NSTimer nefunguje během posouvání

Klasický problém: NSTimer přidaný v režimu .default přestává fungovat během posouvání, protože RunLoop přepne do režimu .tracking a nezpracovává časovače z .default. Řešení — přidejte časovač do .commonModes: RunLoop.current.add(timer, forMode: .common). To způsobí, že časovač bude fungovat jak v .default, tak v .tracking. Alternativou je použití DispatchQueue.main.async místo NSTimer, protože GCD pracuje na úrovni vláken, nikoli režimů RunLoop.

RunLoop ve vláknech na pozadí

Vlákna na pozadí nemají ve výchozím nastavení RunLoop. Pokud je ve vlákně na pozadí třeba spustit NSTimer, zpracovávat NSInputStream/NSOutputStream nebo reagovat na performSelector:, je třeba ručně vytvořit a spustit RunLoop. Bez RunLoop časovač a performSelector: nikdy nebudou fungovat — vlákno spustí kód a skončí bez čekání na události.

Pro vytvoření RunLoop ve vlákně na pozadí stačí zavolat RunLoop.current.run() na konci práce vlákna. Toto volání blokuje vlákno na neurčitou dobu a zpracovává události. Pro zastavení použijte CFRunLoopStop(CFRunLoopGetCurrent()). Důležité: RunLoop.current vytváří RunLoop líně při prvním přístupu — pokud nezavoláte run(), nebude zpracovávat události. Vzor: konfigurace zdrojů -> přidání do RunLoop -> volání run().

swift
import Foundation

// Vlákno na pozadí s vlastním RunLoop
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // RunLoop je automaticky vytvořen při volání RunLoop.current
            let runLoop = RunLoop.current

            // Přidáváme port pro udržení RunLoop aktivního
            runLoop.add(Port(), forMode: .default)

            // Spouštíme zpracování událostí
            var isFinished = false
            while !isFinished {
                // run(mode:before:) vrátí true, pokud byla událost zpracována
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

    func start() {
        thread.start()
        isRunning = true
    }

    func stop() {
        // Zastavení RunLoop ve vlákně na pozadí
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

    @objc
    private func stopRunLoop() {
        CFRunLoopStop(CFRunLoopGetCurrent())
        isRunning = false
    }
}

// Použití: časovač na RunLoop na pozadí
let manager = BackgroundRunLoopManager()
manager.start()

// Odeslání úkolu na RunLoop na pozadí pomocí performSelector
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager vytváří vlákno na pozadí s trvalým RunLoop. Přidání prázdného Port() je nutné, aby RunLoop neukončil okamžitě — bez zdrojů RunLoop.run() vrátí false a skončí. performSelector:onThread: posílá zprávu na RunLoop na pozadí — bude zpracována, až RunLoop vstoupí do fáze BeforeSources. Stop volá CFRunLoopStop na vlákně na pozadí a ukončuje cyklus.

NSTimer vytváří událost časovače, kterou RunLoop zpracovává ve fázi BeforeTimers. Časovače mohou být repeating (opakující se) a non-repeating (jednorázové). NSTimer nezaručuje přesnost: pokud je RunLoop blokován dlouhou operací, časovač se aktivuje po odblokování a všechna zmeškaná aktivace se sloučí do jedné (pro opakující se časovač — nejvýše jedno „dohánějící“ aktivace).

CADisplayLink — specializovaný časovač synchronizovaný s frekvencí obnovování obrazovky (60/120/144 Hz). Používá se pro animace a aktualizace videa. CADisplayLink se přidává do RunLoop a aktivuje se před každým snímkem vykreslování (než Core Animation odešle vrstvu k vykreslení). Pokud je snímek vynechán (display link se nestihl aktivovat do 16 ms), další volání proběhne v dalším cyklu VSync.

swift
import UIKit

class AnimationController {

    private var displayLink: CADisplayLink?
    private var displayLinkTimer: Timer?
    private var startTime: CFTimeInterval = 0

    // CADisplayLink — animace s vsync
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // Přidání v režimu .common — funguje i bēhem posouvání
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // Voláno každý snímek (60 FPS → každých 16.6 ms)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // zastavení po 5 sekundách
        }
    }

    // NSTimer — periodický úkol
    func startTimerInCommonMode() {
        displayLinkTimer?.invalidate()
        displayLinkTimer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: true
        ) { [weak self] timer in
            print("Timer tick")
        }

        // KLÍČ: přidáváme do .common, jinak časovač zamrzne při posouvání
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

    func stop() {
        displayLink?.invalidate()
        displayLinkTimer?.invalidate()
    }
}

V AnimationController je CADisplayLink přidán v režimu .common, což garantuje volání step na každém snímku bez ohledu na posouvání. displayLink.add(to: .current, forMode: .common) — standardní vzor pro animace, které by neměly být přerušeny bēhem posouvání. NSTimer byl také přidán v režimu .common, aby tikala bēhem posouvání. Bez toho by časovač fungoval pouze v režimu .default.

RunLoop Observers: monitorování cyklu událostí

CFRunLoopObserver — mechanismus pro sledování fází RunLoop. Pomocí Observer můžete dostávat oznámení o vstupu do režimu, začátku zpracování časovačů, začátku zpracování zdrojů, probuzení ze spánku, výstupu z režimu. Observer jsou používány frameworky pro své potřeby: Core Animation je používá pro vykreslování vrstev před spánkem RunLoop, UIKit — pro aktualizaci layoutu po zpracování událostí.

Vývojář může také přidávat Observer pro vlastní účely. Například: měření času zpracování událostí (profilování), provádění odložených operací před spánkem RunLoop (když je UI již aktualizováno a uživatel neinteraguje), automatické ukládání dat při dlouhé nečinnosti. Observer se registruje prostřednictvím CFRunLoopAddObserver s uvedením režimu a bitové masky sledovaných aktivit.

Nejužitečnější body pro Observer: .afterWaiting — provádí se po probuzení RunLoop a může obsahovat kód, který by se měl spustit po zpracování události; .beforeTimers — před zpracováním časovačů, umožňuje měření času od předchozího zpracování; .exit — aktivuje se při zastavení RunLoop, užitečné pro čištění zdrojů vlákna na pozadí.

CFRunLoopStop a ukončení cyklu

CFRunLoopStop — funkce, která násilně ukončí aktuální iteraci RunLoop. Při volání CFRunLoopStop(CFRunLoopGetCurrent()) RunLoop dokončí zpracování aktuální události a opustí run(), vracející false. Toto je standardní způsob zastavení RunLoop ve vlákně na pozadí. Na Main Thread se CFRunLoopStop nedoporučuje — hlavní RunLoop by měl fungovat po celou dobu životnosti aplikace. Pro vlákna na pozadí může po CFRunLoopStop vlákno skončit nebo pokračovat v provádění následujícího kódu po run().

Často kladené otázky

Co je RunLoop v iOS?

RunLoop — cyklus událostí v iOS implementovaný CFRunLoop (Core Foundation) a NSRunLoop (Foundation). Čeká na události (dotyky, časovače, zdroje vstupu) a předává je obsluhám ve vlákně. Každé vlákno může mít jeden RunLoop, ale automaticky je vytvářen pouze pro Main Thread. RunLoop spravuje režimy (.default, .tracking, .common), izoluje zpracování podle priorit.

Proč NSTimer nefunguje během posouvání?

NSTimer je ve výchozím nastavení přidán do režimu .default RunLoop. Když uživatel posouvá, RunLoop přepne do režimu .tracking a nezpracovává časovače z .default. Řešení: přidejte časovač do režimu .common pomocí RunLoop.current.add(timer, forMode: .common). .common spojuje .default a .tracking, takže časovač funguje v obou režimech.

Je třeba spustit RunLoop ve vlákně na pozadí?

Pouze pokud vlákno na pozadí používá časovače (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream nebo Source události. Pokud vlákno provádí synchronní úkol (stahování souboru, výpočty) a končí — RunLoop není potřeba. Pro spuštění zavolejte RunLoop.current.run() po konfiguraci zdrojů. Pro zastavení — CFRunLoopStop(CFRunLoopGetCurrent()).

Čím se liší RunLoop od GCD DispatchQueue?

RunLoop pracuje na úrovni vlákna a zpracovává události sekvenčně s podporou režimů (modes). DispatchQueue — abstrakce fondu vláken, úkoly se provádějí na libovolném volném vlákně. GCD nepodporuje režimy a existuje ne závisle na RunLoop. DispatchQueue.main používá hlavní RunLoop k provádění bloků — to je jediný průsečík. Pro úkoly na pozadí je preferován GCD.

Jak souvisí CADisplayLink s RunLoop?

CADisplayLink — časovač synchronizovaný s VSync (frekvencí obnovování obrazovky). Přidává se do RunLoop a aktivuje se před každým snímkem vykreslování ve fázi BeforeTimers. CADisplayLink funguje pouze na Main Thread, protože vykreslování obrazovky probíhá tam. Pro nepřerušované animace bēhem posouvání jej přidejte v režimu .common: displayLink.add(to: .current, forMode: .common).

Shrnutí

  • RunLoop — iOS event loop: zpracovává dotyky, časovače, zdroje vstupu ve vlákně; Main Thread má automatický RunLoop
  • Tři režimy: .default (obecný), .tracking (posouvání), .common (default + tracking) — řídí filtrování událostí
  • NSTimer v .default nefunguje při posouvání — řešení: přidání do režimu .common pomocí RunLoop.current.add(timer, forMode: .common)
  • Vlákna na pozadí nemají ve výchozím nastavení RunLoop — pro časovače a performSelector: je vyžadováno ruční spuštění pomocí RunLoop.current.run()
  • CADisplayLink — časovač pro každý VSync snímek, nezbytný pro plynulé animace; přidává se do .common pro práci bēhem posouvání
  • RunLoop Observer — monitorování fází: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; používá se pro profilování
  • RunLoop ≠ Looper: iOS RunLoop podporuje režimy a časovače, Android Looper je jednodušší — bez režimů, Handler + MessageQueue

Vyvineme mobilní aplikaci na klíč

IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.

Prodiskutovat projekt

Přečtěte si také