RunLoop în iOS: ce este, modurile de lucru și ciclul de evenimente

Autor: IT Sectr Publicat: 2026-03-16 Timp de citire: 11 min

RunLoop — ciclul de procesare a evenimentelor în iOS, implementat prin obiectele CFRunLoop (Core Foundation) și NSRunLoop (Foundation). Este un mecanism care așteaptă evenimente (atingeri, timer-e, surse de intrare, notificări) și le distribuie handler-elor corespunzătoare pe firul de execuție. Fiecare fir de execuție în iOS are cel mult un RunLoop, dar acesta este creat automat doar pentru Main Thread. Conform documentației Apple CFRunLoop, RunLoop este critic pentru funcționarea timer-elor, animațiilor și monitorizării surselor în firele de fundal.

Cele mai importante

  • RunLoop — event loop care procesează evenimente pe fir: atingeri, timer-e, surse de intrare
  • Main Thread are RunLoop automat (CFRunLoopGetMain()), firele de fundal trebuie pornite manual
  • Trei moduri: .default (principal), .tracking (derulare), .common (combină default+tracking)
  • NSTimer și CADisplayLink nu funcționează fără un RunLoop activ pe fir
  • RunLoop observers permit reacția la intrarea/ieșirea din moduri și începutul/sfârșitul procesării

Ce este RunLoop

RunLoop — este un obiect de infrastructură Core Foundation care organizează procesarea evenimentelor pe fir. În esență, este o buclă infinită (while true) care așteaptă sosirea evenimentelor (sources) și le transmite handler-elor. Când nu există evenimente, RunLoop pune firul în stare de somn (sleep), economisind energia bateriei. La sosirea unui eveniment, firul este trezit, îl procesează și adoarme din nou. RunLoop există doar în iOS/macOS (XNU + Core Foundation) — în Android rolul său este îndeplinit de Looper.

Fiecare fir are cel mult un RunLoop, care este creat leneș (lazy) la prima accesare. Pentru Main Thread, RunLoop este creat automat la pornirea aplicației. Pentru firele de fundal, RunLoop nu este creat până când nu este apelat CFRunLoopGetCurrent() sau RunLoop.current. RunLoop principal al aplicației este responsabil pentru procesarea evenimentelor tactile, randarea ecranului, executarea blocurilor DispatchQueue.main și deservirea straturilor Core Animation.

RunLoop nu este un fir — este un mecanism în interiorul firului. Un fir poate exista fără RunLoop (dacă execută o sarcină sincronă și se termină), dar RunLoop nu poate exista fără un fir. Când un fir cu RunLoop activ nu are evenimente, nu blochează CPU, ci se află în stare de așteptare (waiting) — aceasta este diferența cheie față de busy-wait, care consumă 100% CPU.

Cum funcționează RunLoop: anatomia ciclului de evenimente

RunLoop procesează două tipuri de surse de evenimente: Input Sources (surse de intrare) și Timer Sources (timer-e). Input Sources livrează evenimente asincrone: atingeri, mișcări ale mouse-ului, date din socket, mesaje de la alte fire (performSelector:onThread:). Timer Sources livrează evenimente sincrone conform programului: NSTimer, CADisplayLink. Există, de asemenea, Observers — puncte de intrare pentru monitorizarea stării RunLoop.

Ciclul RunLoop constă din faze succesive: intrarea în mod (kCFRunLoopEntry), procesarea timer-elor (kCFRunLoopBeforeTimers), procesarea surselor de intrare (kCFRunLoopBeforeSources), procesarea surselor (kCFRunLoopAfterWaiting), așteptare (sleep), ieșirea din mod (kCFRunLoopExit). Dacă în iterația curentă nu a fost procesat niciun eveniment, RunLoop pune firul în somn pe o perioadă nedeterminată până la trezirea de către un eveniment nou.

swift
import Foundation

// Demonstrarea fazelor RunLoop prin 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 activat")
        case .beforeTimers:
            print("BeforeTimers — procesarea timer-elor")
        case .beforeSources:
            print("BeforeSources — procesarea surselor")
        case .afterWaiting:
            print("AfterWaiting — trezirea după sleep")
        case .exit:
            print("Exit — RunLoop terminat")
        default:
            break
        }
    }

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

// Exemplu: RunLoop procesează un timer pe firul principal
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // RunLoop.current.run() pe Main Thread este apelat de UIApplicationMain
    // automat — nu trebuie pornit manual
    RunLoop.current.run() // Această apelare nu revine pe Main Thread
}

Exemplul observeRunLoopActivities înregistrează un Observer pe RunLoop-ul principal care loghează fiecare fază a ciclului. Acest lucru este util pentru depanare: dacă vedeți un interval lung între .beforeTimers și .afterWaiting, înseamnă că RunLoop este blocat de o operațiune pe Main Thread. timerOnMainRunLoop arată cum NSTimer funcționează automat pe RunLoop-ul principal — la crearea Timer.scheduledTimer, timer-ul este adăugat în RunLoop-ul curent implicit (.default mode).

RunLoop vs Looper (Android)

Android Looper — analogul RunLoop. Looper.prepare() creează o coadă de mesaje (MessageQueue) pe fir, Looper.loop() pornește o buclă infinită de procesare. Handler trimite mesaje și Runnable în această coadă. Diferența principală: RunLoop suportă moduri (modes), iar Android Looper — nu. Looper procesează toate mesajele fără filtrare după mod, ceea ce îl face mai simplu, dar mai puțin flexibil în scenarii cu priorități (de exemplu, derularea în iOS este procesată în modul .tracking separat de alte evenimente).

Modurile RunLoop: default, tracking, common

RunLoop Mode — este un set de surse, timer-e și observatori activi la un moment dat. Modurile permit izolarea procesării evenimentelor în funcție de priorități. Când utilizatorul derulează UITableView, RunLoop comută în modul .tracking, în care sunt procesate doar evenimentele de derulare și timer-ele/animațiile corespunzătoare. Toate celelalte surse (de exemplu, NSURLConnection) sunt suspendate până la ieșirea din modul de derulare.

Trei moduri principale: .default (NSDefaultRunLoopMode) — modul principal în care sunt procesate toate evenimentele cu excepția derulării; .tracking (UITrackingRunLoopMode) — se activează la derulare sau navigare prin gesturi; .common (NSRunLoopCommonModes) — nu este un mod separat, ci un set de aliasuri care include .default + .tracking. Adăugarea unei surse în .commonModes o adaugă automat în toate modurile setului.

ModConstanta Core FoundationConstanta FoundationCând este activ
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultStare normală, fără derulare
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingDerulare, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPseudo-mod: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModePrima pornire a RunLoop

De ce NSTimer nu funcționează în timpul derulării

Problemă clasică: NSTimer adăugat în modul .default încetează să funcționeze în timpul derulării, deoarece RunLoop comută în modul .tracking și nu procesează timer-ele din .default. Soluția — adăugați timer-ul în .commonModes: RunLoop.current.add(timer, forMode: .common). Aceasta va face ca timer-ul să funcționeze atât în .default, cât și în .tracking. Alternativa este utilizarea DispatchQueue.main.async în loc de NSTimer, deoarece GCD funcționează la nivelul firelor, nu al modurilor RunLoop.

RunLoop în firele de fundal

Firele de fundal nu au RunLoop în mod implicit. Dacă în firul de fundal trebuie să porniți NSTimer, să procesați NSInputStream/NSOutputStream sau să reacționați la performSelector:, trebuie să creați și să porniți manual RunLoop. Fără RunLoop, timer-ul și performSelector: nu vor funcționa niciodată — firul va executa codul și se va termina fără să aștepte evenimente.

Pentru a crea RunLoop într-un fir de fundal, este suficient să apelați RunLoop.current.run() la sfârșitul lucrului firului. Această apelare blochează firul pe termen nedeterminat, procesând evenimente. Pentru oprire, utilizați CFRunLoopStop(CFRunLoopGetCurrent()). Important: RunLoop.current creează RunLoop leneș la prima accesare — dacă nu apelați run(), nu va procesa evenimente. Model: configurarea surselor -> adăugarea în RunLoop -> apelarea run().

swift
import Foundation

// Fir de fundal cu RunLoop propriu
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // RunLoop este creat automat la apelarea RunLoop.current
            let runLoop = RunLoop.current

            // Adăugăm un port pentru menținerea RunLoop activ
            runLoop.add(Port(), forMode: .default)

            // Pornim procesarea evenimentelor
            var isFinished = false
            while !isFinished {
                // run(mode:before:) returnează true dacă un eveniment a fost procesat
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

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

    func stop() {
        // Oprirea RunLoop pe firul de fundal
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

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

// Utilizare: timer pe RunLoop de fundal
let manager = BackgroundRunLoopManager()
manager.start()

// Trimiterea unei sarcini către RunLoop de fundal prin performSelector
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager creează un fir de fundal cu RunLoop permanent. Adăugarea unui Port() gol este necesară pentru a preveni terminarea imediată a RunLoop — fără surse, RunLoop.run() returnează false și iese. performSelector:onThread: trimite un mesaj către RunLoop-ul de fundal — va fi procesat când RunLoop intră în faza BeforeSources. Stop apelează CFRunLoopStop pe firul de fundal, încheind ciclul.

NSTimer creează un eveniment de timer pe care RunLoop îl procesează în faza BeforeTimers. Timer-ele pot fi repeating (repetabile) și non-repeating (unice). NSTimer nu garantează precizia declanșării: dacă RunLoop este blocat de o operațiune lungă, timer-ul va declanșa după deblocare, iar toate declanșările pierdute vor fi combinate într-una singură (pentru timer-ul repetabil — cel mult o declanșare „recuperată”).

CADisplayLink — un timer specializat sincronizat cu rata de reîmprospătare a ecranului (60/120/144 Hz). Este utilizat pentru animații și actualizări video. CADisplayLink este adăugat în RunLoop și declanșează înainte de fiecare cadru de randare (înainte ca Core Animation să trimită stratul la randare). Dacă un cadru este omis (display link nu a reușit să declanșeze în 16 ms), următoarea apelare are loc în următorul ciclu VSync.

swift
import UIKit

class AnimationController {

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

    // CADisplayLink — animație cu vsync
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // Adăugarea în modul .common — funcționează și în timpul derulării
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // Apelat la fiecare cadru (60 FPS → la fiecare 16.6 ms)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // oprire după 5 secunde
        }
    }

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

        // CHEIE: adăugăm în .common, altfel timer-ul îngheață la derulare
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

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

În AnimationController, CADisplayLink este adăugat în modul .common, ceea ce garantează apelarea step la fiecare cadru indiferent de derulare. displayLink.add(to: .current, forMode: .common) — modelul standard pentru animațiile care nu trebuie întrerupte în timpul derulării. NSTimer a fost, de asemenea, adăugat în modul .common pentru a tica în timpul derulării. Fără aceasta, timer-ul ar funcționa doar în modul .default.

RunLoop Observers: monitorizarea ciclului de evenimente

CFRunLoopObserver — mecanism pentru urmărirea fazelor RunLoop. Cu ajutorul Observer, puteți primi notificări despre intrarea în mod, începutul procesării timer-elor, începutul procesării surselor, trezirea după somn, ieșirea din mod. Observer-ii sunt utilizați de framework-uri pentru nevoile lor: Core Animation îi folosește pentru randarea straturilor înainte de somnul RunLoop, UIKit — pentru actualizarea layout-ului după procesarea evenimentelor.

Dezvoltatorul poate adăuga, de asemenea, Observer-i pentru propriile scopuri. De exemplu: măsurarea timpului de procesare a evenimentelor (profilare), executarea operațiunilor amânate înainte de somnul RunLoop (când UI este deja actualizat și utilizatorul nu interacționează), salvarea automată a datelor în timpul inactivității prelungite. Observer-ul se înregistrează prin CFRunLoopAddObserver cu specificarea modului și a măștii de biți a activităților urmărite.

Cele mai utile puncte pentru Observer: .afterWaiting — se execută după trezirea RunLoop și poate conține cod care ar trebui să ruleze după procesarea evenimentului; .beforeTimers — înainte de procesarea timer-elor, permite măsurarea timpului scurs de la procesarea anterioară; .exit — se declanșează la oprirea RunLoop, util pentru curățarea resurselor firului de fundal.

CFRunLoopStop și încheierea ciclului

CFRunLoopStop — funcție care forțează terminarea iterației curente a RunLoop. La apelarea CFRunLoopStop(CFRunLoopGetCurrent()), RunLoop termină procesarea evenimentului curent și iese din run(), returnând false. Aceasta este metoda standard de oprire a RunLoop pe un fir de fundal. Pe Main Thread, CFRunLoopStop nu este recomandat — RunLoop-ul principal trebuie să funcționeze pe întreaga durată de viață a aplicației. Pentru firele de fundal, după CFRunLoopStop firul se poate termina sau continua execuția codului următor după run().

Întrebări frecvente

Ce este RunLoop în iOS?

RunLoop — ciclul de evenimente în iOS implementat de CFRunLoop (Core Foundation) și NSRunLoop (Foundation). Așteaptă evenimente (atingeri, timer-e, surse de intrare) și le distribuie handler-elor pe fir. Fiecare fir poate avea un RunLoop, dar automat este creat doar pentru Main Thread. RunLoop gestionează modurile (.default, .tracking, .common), izolând procesarea după priorități.

De ce NSTimer nu funcționează în timpul derulării?

NSTimer în mod implicit este adăugat în modul .default al RunLoop. Când utilizatorul derulează, RunLoop comută în modul .tracking și nu procesează timer-ele din .default. Soluția: adăugați timer-ul în modul .common prin RunLoop.current.add(timer, forMode: .common). .common combină .default și .tracking, astfel timer-ul funcționează în ambele moduri.

Trebuie să pornesc RunLoop într-un fir de fundal?

Doar dacă firul de fundal utilizează timer-e (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream sau evenimente Source. Dacă firul execută o sarcină sincronă (descărcare fișier, calcule) și se termină — RunLoop nu este necesar. Pentru pornire, apelați RunLoop.current.run() după configurarea surselor. Pentru oprire — CFRunLoopStop(CFRunLoopGetCurrent()).

Cu ce se diferențiază RunLoop de GCD DispatchQueue?

RunLoop funcționează la nivelul firului și procesează evenimentele secvențial, cu suport pentru moduri (modes). DispatchQueue — o abstractizare a unui pool de fire, sarcinile sunt executate pe orice fir liber. GCD nu suportă moduri și există independent de RunLoop. DispatchQueue.main utilizează RunLoop-ul principal pentru executarea blocurilor — acesta este singurul punct de intersecție. Pentru sarcinile de fundal, GCD este preferat.

Cum este legat CADisplayLink de RunLoop?

CADisplayLink — este un timer sincronizat cu VSync (rata de reîmprospătare a ecranului). Este adăugat în RunLoop și declanșează înainte de fiecare cadru de randare în faza BeforeTimers. CADisplayLink funcționează doar pe Main Thread, deoarece randarea ecranului are loc acolo. Pentru animații continue în timpul derulării, adăugați-l în modul .common: displayLink.add(to: .current, forMode: .common).

Concluzii

  • RunLoop — bucla de evenimente iOS: procesează atingeri, timer-e, surse de intrare pe fir; Main Thread are RunLoop automat
  • Trei moduri: .default (general), .tracking (derulare), .common (default + tracking) — gestionează filtrarea evenimentelor
  • NSTimer în .default nu funcționează la derulare — soluția: adăugarea în modul .common prin RunLoop.current.add(timer, forMode: .common)
  • Firele de fundal nu au RunLoop implicit — pentru timer-e și performSelector: este necesară pornirea manuală prin RunLoop.current.run()
  • CADisplayLink — timer pentru fiecare cadru VSync, esențial pentru animații fluide; adăugat în .common pentru funcționare în timpul derulării
  • RunLoop Observer — monitorizarea fazelor: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; utilizat pentru profilare
  • RunLoop ≠ Looper: iOS RunLoop suportă moduri și timer-e, Android Looper este mai simplu — fără moduri, Handler + MessageQueue

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și