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 — 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.
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.
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).
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).
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.
| Mod | Constanta Core Foundation | Constanta Foundation | Când este activ |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Stare normală, fără derulare |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Derulare, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-mod: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Prima pornire a RunLoop |
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.
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().
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.
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.
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 — 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
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.
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.
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()).
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.
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
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.
Citiți și