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 — 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.
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í.
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).
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í).
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žim | Konstanta Core Foundation | Konstanta Foundation | Kdy je aktivní |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Normální stav, bez posouvání |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Posouvání, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-režim: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | První spuštění RunLoop |
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.
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().
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.
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.
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 — 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
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.
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.
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()).
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.
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í
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í.
Přečtěte si také