RunLoop iOS-ban: mi ez, munkamódok és eseményciklus

Szerző: IT Sectr Megjelenés: 2026-03-16 Olvasási idő: 11 perc

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 — event loop, amely a szálon dolgozza fel az eseményeket: érintések, időzítők, bemeneti források
  • Main Thread automatikus RunLoop-pal rendelkezik (CFRunLoopGetMain()), a háttérszálakat kézzel kell elindítani
  • Három mód: .default (fő), .tracking (görgetés), .common (egyesíti a default+tracking-et)
  • NSTimer és CADisplayLink nem működik aktív RunLoop nélkül a szálon
  • RunLoop observers lehetővé teszik a módokba való belépésre/kilépésre és a feldolgozás kezdetére/végére való reagálást

Mi az a RunLoop

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.

Hogyan működik a RunLoop: az eseményciklus anatómiája

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.

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

RunLoop vs Looper (Android)

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 Módok: default, tracking, common

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ódCore Foundation állandóFoundation állandóMikor aktív
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultNormál állapot, görgetés nélkül
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingGörgetés, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPszeudo-mód: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeA RunLoop első indítása

Miért nem működik az NSTimer görgetés közben

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.

RunLoop háttérszálakban

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.

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

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

RunLoop Observers: az eseményciklus figyelése

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 és a ciklus befejezése

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

Mi az a RunLoop iOS-ban?

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.

Miért nem működik az NSTimer görgetés közben?

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.

Kell RunLoop-ot indítani háttérszálban?

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

Miben különbözik a RunLoop a GCD DispatchQueue-tól?

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.

Hogyan kapcsolódik a CADisplayLink a RunLoop-hoz?

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ó

  • RunLoop — iOS event loop: érintéseket, időzítőket, bemeneti forrásokat dolgoz fel a szálon; Main Thread automatikus RunLoop-pal rendelkezik
  • Három mód: .default (általános), .tracking (görgetés), .common (default + tracking) — kezelik az esemény szűrést
  • NSTimer .default-ban nem működik görgetés közben — megoldás: hozzáadás .common módba a RunLoop.current.add(timer, forMode: .common) segítségével
  • Háttérszálak alapértelmezésben nem rendelkeznek RunLoop-pal — időzítőkhöz és performSelector:hoz kézi indítás szükséges a RunLoop.current.run() segítségével
  • CADisplayLink — időzítő minden VSync kockához, elengedhetetlen a sima animációkhoz; .common módba adva görgetés közben is működik
  • RunLoop Observer — fázisok figyelése: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; profilozáshoz használják
  • RunLoop ≠ Looper: iOS RunLoop támogatja a módokat és időzítőket, Android Looper egyszerűbb — módok nélkül, Handler + MessageQueue

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.

Projekt megbeszélése

Olvassa el is