RunLoop — eseményfeldolgozási ciklus iOS-ban, a CFRunLoop (Core Foundation) és NSRunLoop (Foundation) objektumok által megvalósítva. Ez egy mechanizmus, amely eseményeket vár (érintések, időzítők, bemeneti források, értesítések) és továbbítja azokat a megfelelő kezelőkhöz a szálon. Minden szál iOS-ben legfeljebb egy RunLoop-pal rendelkezik, de az automatikusan csak a Main Thread számára jön létre. Az Apple CFRunLoop dokumentációja szerint a RunLoop kritikus fontosságú az időzítők, animációk és forrásfigyelés működéséhez a háttérszálakban.
Legfontosabb tudnivalók
RunLoop — egy Core Foundation infrastruktúra objektum, amely az események feldolgozását szervezi a szálon. Lényegében egy végtelen ciklus (while true), amely várja az események (sources) érkezését és továbbítja azokat a kezelőkhöz. Ha nincsenek események, a RunLoop alvásba helyezi a szálat (sleep), energiát takarítva meg. Esemény érkezésekor a szál felébred, feldolgozza azt és újra elalszik. A RunLoop csak iOS/macOS (XNU + Core Foundation) rendszerben létezik — Androidban a Looper játssza a szerepét.
Minden szálnak legfeljebb egy RunLoop-ja van, amely lustán (lazy) jön létre az első hozzáféréskor. A Main Thread számára a RunLoop automatikusan létrejőn az alkalmazás indításakor. Háttérszálak számára a RunLoop nem jőn létre, amíg a CFRunLoopGetCurrent() vagy a RunLoop.current meg nem hívásra kerül. Az alkalmazás fő RunLoop-ja felelős az érintési események feldolgozásáért, a képernyő megjelenítéséért, a DispatchQueue.main blokkok végrehajtásáért és a Core Animation rétegek kiszolgálásáért.
A RunLoop nem szál — ez egy mechanizmus a szálon belül. A szál létezhet RunLoop nélkül (ha szinkron feladatot hajt végre és befejezi), de a RunLoop nem létezhet szál nélkül. Amikor egy aktív RunLoop-pal rendelkező szálnak nincsenek eseményei, nem blokkolja a CPU-t, hanem várakozási állapotban (waiting) van — ez a fő különbség a busy-wait-tel szemben, amely 100% CPU-t használ.
RunLoop két típusú eseményforrást dolgoz fel: Input Sources (bemeneti források) és Timer Sources (időzítők). Az Input Sources aszinkron eseményeket szállít: érintések, egérmozgások, adatok socketből, üzenetek más szálaktól (performSelector:onThread:). A Timer Sources szinkron eseményeket szállít ütemezés szerint: NSTimer, CADisplayLink. Léteznek továbbá Observers — belépési pontok a RunLoop állapotának figyeléséhez.
A RunLoop ciklus egymást követő fázisokból áll: belépés a módba (kCFRunLoopEntry), időzítők feldolgozása (kCFRunLoopBeforeTimers), bemeneti források feldolgozása (kCFRunLoopBeforeSources), források feldolgozása (kCFRunLoopAfterWaiting), várakozás (sleep), kilépés a módból (kCFRunLoopExit). Ha az aktuális iterációban nem került feldolgozásra egyetlen esemény sem, a RunLoop határozatlan ideig alvásba helyezi a szálat, amíg egy új esemény fel nem ébreszti.
import Foundation
// RunLoop fázisainak bemutatása Observer segítségével
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 aktiválva")
case .beforeTimers:
print("BeforeTimers — időzítők feldolgozása")
case .beforeSources:
print("BeforeSources — források feldolgozása")
case .afterWaiting:
print("AfterWaiting — ébredés alvás után")
case .exit:
print("Exit — RunLoop befejeződött")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Példa: RunLoop feldolgoz egy időzítőt a fő szálon
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// A RunLoop.current.run() a Main Thread-en az UIApplicationMain által kerül meghívásra
// automatikusan — nem kell kézzel elindítani
RunLoop.current.run() // Ez a hívás nem tér vissza a Main Thread-re
}
Az observeRunLoopActivities példa egy Observer-t regisztrál a fő RunLoop-on, amely naplózza a ciklus minden fázisát. Ez hasznos hibakereséshez: ha hosszú intervallumot lát a .beforeTimers és .afterWaiting között, az azt jelenti, hogy a RunLoop egy művelet által blokkolva van a Main Thread-en. A timerOnMainRunLoop megmutatja, hogyan működik automatikusan az NSTimer a fő RunLoop-on — a Timer.scheduledTimer létrehozásakor az időzítő alapértelmezés szerint (.default mode) hozzáadódik az aktuális RunLoop-hoz.
Android Looper — a RunLoop megfelelője. A Looper.prepare() üzenetsort (MessageQueue) hoz létre a szálon, a Looper.loop() elindít egy végtelen feldolgozási ciklust. A Handler üzeneteket és Runnable-okat küld ebbe a sorba. A fő különbség: a RunLoop támogatja a módokat (modes), míg az Android Looper nem. A Looper minden üzenetet mód szerinti szűrés nélkül dolgoz fel, ami egyszerűbbé, de kevésbé rugalmassá teszi prioritásos forgatókönyvekben (pl. a görgetés iOS-ben a .tracking módban külön történik a többi eseménytől).
RunLoop Mode — az adott pillanatban aktív források, időzítők és megfigyelők halmaza. A módok lehetővé teszik az eseményfeldolgozás elkülönítését prioritások szerint. Amikor a felhasználó görgeti a UITableView-t, a RunLoop .tracking módba kapcsol, amelyben csak a görgetési események és a megfelelő időzítők/animációk kerülnek feldolgozásra. Az összes többi forrás (pl. NSURLConnection) felfüggesztésre kerül a görgetési módból való kilépésig.
Három fő mód: .default (NSDefaultRunLoopMode) — a fő mód, amelyben az összes esemény a görgetés kivételével feldolgozásra kerül; .tracking (UITrackingRunLoopMode) — görgetés vagy gesztus navigáció során aktiválódik; .common (NSRunLoopCommonModes) — nem külön mód, hanem álnevek halmaza, amely tartalmazza a .default és .tracking elemeket. Forrás .commonModes-hoz adása automatikusan hozzáadja azt a halmaz összes módjához.
| Mód | Core Foundation állandó | Foundation állandó | Mikor aktív |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Normál állapot, görgetés nélkül |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Görgetés, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pszeudo-mód: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | A RunLoop első indítása |
Klasszikus probléma: a NSTimer .default módba adott időzítő leáll görgetés közben, mert a RunLoop .tracking módba kapcsol és nem dolgozza fel a .default időzítőit. Megoldás — adja hozzá az időzítőt a .commonModes-hoz: RunLoop.current.add(timer, forMode: .common). Ez biztosítja, hogy az időzítő mind .default, mind .tracking módban működjön. Alternatíva a DispatchQueue.main.async használata NSTimer helyett, mivel a GCD szál szinten működik, nem RunLoop módok szintjén.
A háttérszálak alapértelmezésben nem rendelkeznek RunLoop-pal. Ha egy háttérszálon NSTimer-t kell indítani, NSInputStream/NSOutputStream-t kell feldolgozni, vagy performSelector:-ra kell reagálni, kézzel kell létrehozni és elindítani a RunLoopot. RunLoop nélkül az időzítő és a performSelector: soha nem fognak működni — a szál elindítja a kódot és befejezi anélkül, hogy várna az eseményekre.
RunLoop létrehozásához egy háttérszálon elegendő a RunLoop.current.run() meghívása a szál munkájának végén. Ez a hívás határozatlan ideig blokkolja a szálat, feldolgozva az eseményeket. A leállításhoz használja a CFRunLoopStop(CFRunLoopGetCurrent()) függvényt. Fontos: a RunLoop.current lustán hozza létre a RunLoop-ot az első hozzáféréskor — ha nem hívja meg a run()-t, nem fog eseményeket feldolgozni. Minta: források konfigurálása -> hozzáadás a RunLoop-hoz -> run() meghívása.
import Foundation
// Háttérszál saját RunLoop-pal
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop automatikusan létrejőn a RunLoop.current meghívásakor
let runLoop = RunLoop.current
// Port hozzáadása a RunLoop aktívan tartásához
runLoop.add(Port(), forMode: .default)
// Elindítjuk az eseményfeldolgozást
var isFinished = false
while !isFinished {
// A run(mode:before:) true-t ad vissza, ha esemény került feldolgozásra
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// RunLoop leállítása háttérszálon
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Használat: időzítő háttér RunLoop-on
let manager = BackgroundRunLoopManager()
manager.start()
// Feladat küldése háttér RunLoop-ra performSelector segítségével
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
A BackgroundRunLoopManager egy háttérszálat hoz lére állandó RunLoop-pal. Egy üres Port() hozzáadása szükséges a RunLoop azonnali befejeződésének megakadályozásához — források nélkül a RunLoop.run() false-t ad vissza és kilép. A performSelector:onThread: üzenetet küld a háttér RunLoop-ra — az feldolgozásra kerül, amikor a RunLoop belép a BeforeSources fázisba. A Stop meghívja a CFRunLoopStop-ot a háttérszálon, befejezve a ciklust.
NSTimer egy időzítő eseményt hoz létre, amelyet a RunLoop a BeforeTimers fázisban dolgoz fel. Az időzítők lehetnek repeating (ismétlődő) és non-repeating (egyszeri). Az NSTimer nem garantálja a pontosságot: ha a RunLoop egy hosszú művelet által blokkolva van, az időzítő a blokkolás feloldása után aktiválódik, és az összes kihagyott aktiválódás egyesül (ismétlődő időzítő esetén — legfeljebb egy „utolérési“ aktiválódás).
CADisplayLink — egy speciális időzítő, amely szinkronizálva van a képernyő frissítési frekvenciájával (60/120/144 Hz). Animációk és videó frissítésekhez használják. A CADisplayLink hozzáadódik a RunLoop-hoz és minden egyes megjelenítési kocka előtt aktiválódik (mielőtt a Core Animation elküldené a réteget megjelenítésre). Ha egy kocka kimarad (a display link nem tudott aktiválódni 16 ms alatt), a következő hívás a következő VSync ciklusban történik.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — animáció vsync-kel
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Hozzáadás .common módban — görgetés közben is működik
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Minden kockán meghívásra kerül (60 FPS → 16.6 ms-ként)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // leállítás 5 másodperc után
}
}
// NSTimer — periodikus feladat
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// KULCS: .common módba adjuk, különben az időzítő lefagy görgetéskor
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
Az AnimationController-ben a CADisplayLink .common módba kerül hozzáadásra, ami garantálja a step meghívását minden kockán, függetlenül a görgetéstől. A displayLink.add(to: .current, forMode: .common) — a szabványos minta azokhoz az animációkhoz, amelyeket nem szabad megszakítani görgetés közben. Az NSTimer is hozzáadásra került a .common módba, hogy ketyegjen görgetés közben. E nélkül az időzítő csak .default módban működne.
CFRunLoopObserver — mechanizmus a RunLoop fázisainak követésére. Az Observer segítségével értesítéseket kaphat a módba való belépésről, az időzítők feldolgozásának kezdetéről, a források feldolgozásának kezdetéről, az alvásból való ébredésről, a módból való kilépésről. Az Observer-eket a keretrendszerek saját céljaikra használják: a Core Animation a rétegek megjelenítéséhez használja őket a RunLoop alvása előtt, az UIKit — a layout frissítéséhez az események feldolgozása után.
A fejlesztő is hozzáadhat Observer-eket saját céljaira. Például: eseményfeldolgozási idő mérése (profilozás), halasztott műveletek végrehajtása a RunLoop alvása előtt (amikor a UI már frissítve van és a felhasználó nem lép kapcsolatba), adatok automatikus mentése hosszú inaktivitás esetén. Az Observer a CFRunLoopAddObserver segítségével regisztrálható a mód és a követett tevékenységek bitmaszkjának megadásával.
Az Observer számára leghasznosabb pontok: .afterWaiting — a RunLoop ébredése után hajtódik végre, és tartalmazhatja az esemény feldolgozása után futó kódot; .beforeTimers — az időzítők feldolgozása előtt, lehetővé teszi az előző feldolgozás óta eltelt idő mérését; .exit — a RunLoop leállásakor aktiválódik, hasznos a háttérszál erőforrásainak tisztításához.
CFRunLoopStop — függvény, amely erőszakkal befejezi az aktuális RunLoop iterációt. A CFRunLoopStop(CFRunLoopGetCurrent()) meghívásakor a RunLoop befejezi az aktuális esemény feldolgozását és kilép a run()-ból, false-t visszaadva. Ez a szabványos módszer a RunLoop leállítására egy háttérszálon. A Main Thread-en a CFRunLoopStop nem ajánlott — a fő RunLoop-nak az alkalmazás teljes élettartama alatt működnie kell. Háttérszálak esetén a CFRunLoopStop után a szál befejeződhet vagy folytathatja a run() utáni kód végrehajtását.
Gyakran Ismételt Kérdések
RunLoop — eseményciklus iOS-ban, a CFRunLoop (Core Foundation) és NSRunLoop (Foundation) által megvalósítva. Eseményeket vár (érintések, időzítők, bemeneti források) és továbbítja azokat a kezelőkhöz a szálon. Minden szál rendelkezhet egy RunLoop-pal, de automatikusan csak a Main Thread számára jön létre. A RunLoop kezeli a módokat (.default, .tracking, .common), elkülönítve a feldolgozást prioritások szerint.
NSTimer alapértelmezésben a RunLoop .default módjába kerül hozzáadásra. Amikor a felhasználó görget, a RunLoop .tracking módba kapcsol és nem dolgozza fel a .default időzítőit. Megoldás: adja hozzá az időzítőt a .common módhoz a RunLoop.current.add(timer, forMode: .common) segítségével. A .common egyesíti a .default és .tracking módokat, így az időzítő mindkét módban működik.
Csak akkor, ha a háttérszál időzítőket (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream vagy Source eseményeket használ. Ha a szál szinkron feladatot (fájl letöltése, számítások) végez és befejezi — RunLoop nem szükséges. Az indításhoz hívja meg a RunLoop.current.run()-t a források konfigurálása után. A leállításhoz — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop szál szinten működik és az eseményeket szekvenciálisan dolgozza fel, módok (modes) támogatásával. DispatchQueue — szálkészlet absztrakció, a feladatok bármely szabad szálon végrehajtódnak. A GCD nem támogat módokat és a RunLoop-tól függetlenül létezik. A DispatchQueue.main a fő RunLoop-ot használja blokkok végrehajtásához — ez az egyetlen metszéspont. Háttérfeladatokhoz a GCD előnyösebb.
CADisplayLink — egy időzítő, amely szinkronizálva van a VSync-cel (képernyő frissítési frekvenciájával). Hozzáadódik a RunLoop-hoz és minden egyes megjelenítési kocka előtt aktiválódik a BeforeTimers fázisban. A CADisplayLink csak a Main Thread-en működik, mert a képernyő megjelenítése ott történik. Folyamatos animációkhoz görgetés közben adja hozzá .common módban: displayLink.add(to: .current, forMode: .common).
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is