RunLoop — циклус обраде догађаја у iOS-у, имплементиран од стране CFRunLoop (Core Foundation) и NSRunLoop (Foundation). Ово је механизам који чека догађаје (додири, тајмери, извори улаза, обавештења) и прослеђује их одговарајућим руковаоцима на нити. Свака нит у iOS-у има највише један RunLoop, али се он аутоматски креира само за Main Thread. Према Apple CFRunLoop документацији, RunLoop је критичан за рад тајмера, анимација и надгледање извора у позадинским нитима.
Главно
RunLoop — инфраструктурни објекат Core Foundation-а који организује обраду догађаја на нити. У суштини, то је бесконачна петља (while true) која чека пристизање догађаја (sources) и прослеђује их руковаоцима. Када нема догађаја, RunLoop успављује нит (sleep), штедећи енергију батерије. По пристизању догађаја, нит се буди, обрађује га и поново заспи. RunLoop постоји само у iOS/macOS (XNU + Core Foundation) — у Android-у његову улогу има Looper.
Свака нит има највише један RunLoop, који се креира лењо (lazy) при првом приступу. За Main Thread, RunLoop се креира аутоматски при покретању апликације. За позадинске нити, RunLoop се не креира док се не позове CFRunLoopGetCurrent() или RunLoop.current. Главни RunLoop апликације је одговоран за обраду додирних догађаја, рендеровање екрана, извршавање блокова DispatchQueue.main и услуживање слојева Core Animation.
RunLoop није нит — то је механизам унутар нити. Нит може постојати без RunLoop (ако извршава синхрони задатак и завршава), али RunLoop не може постојати без нити. Када нит са активним RunLoop-ом нема догађаја, не блокира CPU, већ је у стању чекања (waiting) — то је кључна разлика у односу на busy-wait који троши 100% CPU-а.
RunLoop обрађује два типа извора догађаја: Input Sources (улазни извори) и Timer Sources (тајмери). Input Sources достављају асинхроне догађаје: додири, покрети миша, подаци из сокета, поруке од других нити (performSelector:onThread:). Timer Sources достављају синхроне догађаје по распореду: NSTimer, CADisplayLink. Такође постоје Observers — улазне тачке за надгледање стања RunLoop-а.
Циклус RunLoop-а се састоји од узастопних фаза: улазак у режим (kCFRunLoopEntry), обрада тајмера (kCFRunLoopBeforeTimers), обрада улазних извора (kCFRunLoopBeforeSources), обрада извора (kCFRunLoopAfterWaiting), чекање (sleep), излазак из режима (kCFRunLoopExit). Ако у тренутној итерацији није обрађен ниједан догађај, RunLoop успављује нит на неодређено време до буђења новим догађајем.
import Foundation
// Демонстрација фаза RunLoop-а кроз 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 активиран")
case .beforeTimers:
print("BeforeTimers — обрада тајмера")
case .beforeSources:
print("BeforeSources — обрада извора")
case .afterWaiting:
print("AfterWaiting — буђење из сна")
case .exit:
print("Exit — RunLoop завршен")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Пример: RunLoop обрађује тајмер на главној нити
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// RunLoop.current.run() на Main Thread-у позива UIApplicationMain
// аутоматски — не треба ручно покретати
RunLoop.current.run() // Овај позив се не враћа на Main Thread
}
Пример observeRunLoopActivities региструје Observer на главном RunLoop-у који бележи сваку фазу циклуса. Ово је корисно за отклањање грешака: ако видите дуг интервал између .beforeTimers и .afterWaiting, значи да је RunLoop блокиран операцијом на Main Thread-у. timerOnMainRunLoop показује како NSTimer аутоматски ради на главном RunLoop-у — приликом креирања Timer.scheduledTimer, тајмер се додаје у тренутни RunLoop подразумевано (.default mode).
Android Looper — аналог RunLoop-а. Looper.prepare() креира ред порука (MessageQueue) на нити, Looper.loop() покреће бесконачну петљу обраде. Handler шаље поруке и Runnable у овај ред. Главна разлика: RunLoop подржава режиме (modes), а Android Looper не. Looper обрађује све поруке без филтрирања по режиму, што га чини једноставнијим, али мање флексибилним за сценарије са приоритетима (нпр. скрол у iOS-у се обрађује у .tracking режиму одвојено од других догађаја).
RunLoop Mode — скуп извора, тајмера и посматрача који су активни у датом тренутку. Режими омогућавају изоловање обраде догађаја по приоритетима. Када корисник скролује UITableView, RunLoop прелази у .tracking режим, у коме се обрађују само догађаји скроловања и одговарајући тајмери/анимације. Сви остали извори (нпр. NSURLConnection) се заустављају до изласка из режима скроловања.
Три главна режима: .default (NSDefaultRunLoopMode) — главни режим у коме се обрађују сви догађаји осим скроловања; .tracking (UITrackingRunLoopMode) — активира се при скроловању или гестовној навигацији; .common (NSRunLoopCommonModes) — није посебан режим, већ скуп псеудонима који укључује .default + .tracking. Додавање извора у .commonModes аутоматски га додаје у све режиме скупа.
| Режим | Core Foundation константа | Foundation константа | Када је активан |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Нормално стање, без скроловања |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Скроловање, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Псеудо-режим: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Прво покретање RunLoop-а |
Класичан проблем: NSTimer додат у .default режим престаје да ради током скроловања, јер RunLoop прелази у .tracking режим и не обрађује тајмере из .default. Решење — додајте тајмер у .commonModes: RunLoop.current.add(timer, forMode: .common). То ће учинити да тајмер ради и у .default и у .tracking. Алтернатива је коришћење DispatchQueue.main.async уместо NSTimer, пошто GCD ради на нивоу нити, а не RunLoop режима.
Позадинске нити немају RunLoop подразумевано. Ако у позадинској нити треба покренути NSTimer, обрађивати NSInputStream/NSOutputStream или реаговати на performSelector:, потребно је ручно креирати и покренути RunLoop. Без RunLoop-а, тајмер и performSelector: никада неће радити — нит ће покренути код и завршити се без чекања на догађаје.
За креирање RunLoop-а у позадинској нити довољно је позвати RunLoop.current.run() на крају рада нити. Овај позив блокира нит на неодређено време, обрађујући догађаје. За заустављање користите CFRunLoopStop(CFRunLoopGetCurrent()). Важно: RunLoop.current креира RunLoop лењо при првом приступу — ако не позовете run(), неће обрађивати догађаје. Образац: конфигурација извора -> додавање у RunLoop -> позивање run().
import Foundation
// Позадинска нит са сопственим RunLoop-ом
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop се креира аутоматски при позиву RunLoop.current
let runLoop = RunLoop.current
// Додајемо порт за одржавање RunLoop-а активним
runLoop.add(Port(), forMode: .default)
// Покрећемо обраду догађаја
var isFinished = false
while !isFinished {
// run(mode:before:) враћа true ако је догађај обрађен
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Заустављање RunLoop-а на позадинској нити
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Коришћење: тајмер на позадинском RunLoop-у
let manager = BackgroundRunLoopManager()
manager.start()
// Слање задатка на позадински RunLoop кроз performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager креира позадинску нит са сталним RunLoop-ом. Додавање празног Port()-а је неопходно да RunLoop не би одмах завршио — без извора, RunLoop.run() враћа false и излази. performSelector:onThread: шаље поруку на позадински RunLoop — биће обрађена када RunLoop уђе у фазу BeforeSources. Stop позива CFRunLoopStop на позадинској нити, завршавајући циклус.
NSTimer креира догађај-тајмер који RunLoop обрађује у фази BeforeTimers. Тајмери могу бити repeating (понављајући) и non-repeating (једнократни). NSTimer не гарантује прецизност окидања: ако је RunLoop блокиран дугом операцијом, тајмер ће се окинути након деблокирања, а сва пропуштена окидања ће се објединити у једно (за repeating тајмер — највише једно „накнадно“ окидање).
CADisplayLink — специјализовани тајмер синхронизован са фреквенцијом освежавања екрана (60/120/144 Hz). Користи се за анимације и ажурирање видеа. CADisplayLink се додаје у RunLoop и окида се пре сваког кадра рендеровања (пре него што Core Animation пошаље слој на рендеровање). Ако је кадар пропуштен (display link није стигао да се окине за 16 ms), следећи позив се дешава у следећем VSync циклусу.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — анимација са vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Додавање у .common режим — ради и при скроловању
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Позива се сваки кадар (60 FPS → сваких 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // заустављање након 5 секунди
}
}
// NSTimer — периодични задатак
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// КЉУЧ: додајемо у .common, иначе тајмер замрзава при скроловању
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
У AnimationController-у, CADisplayLink је додат у .common режим, што гарантује позив step на сваком кадру независно од скроловања. displayLink.add(to: .current, forMode: .common) — стандардни образац за анимације које не треба да буду прекинуте при скроловању. NSTimer је такође додат у .common режим да би откуцавао током скроловања. Без овога, тајмер би радио само у .default режиму.
CFRunLoopObserver — механизам за праћење фаза RunLoop-а. Помоћу Observer-а можете добијати обавештења о уласку у режим, почетку обраде тајмера, почетку обраде извора, буђењу из сна, изласку из режима. Observer-и се користе од стране framework-а за своје потребе: Core Animation их користи за рендеровање слојева пре спавања RunLoop-а, UIKit — за ажурирање layout-а након обраде догађаја.
Програмер такође може додавати Observer-е за своје потребе. На пример: мерење времена обраде догађаја (профилисање), извршавање одложених операција пре спавања RunLoop-а (када је UI већ ажуриран и корисник не интерагује), аутоматско чување података при дугом неактивности. Observer се региструје кроз CFRunLoopAddObserver са навођењем режима и бит маске праћених активности.
Најкорисније тачке за Observer: .afterWaiting — извршава се након буђења RunLoop-а и може садржати код који треба да се покрене након обраде догађаја; .beforeTimers — пре обраде тајмера, омогућава мерење времена протеклог од претходне обраде; .exit — окида се при заустављању RunLoop-а, корисно за чишћење ресурса позадинске нити.
CFRunLoopStop — функција која присилно завршава тренутну итерацију RunLoop-а. При позиву CFRunLoopStop(CFRunLoopGetCurrent()), RunLoop завршава обраду тренутног догађаја и излази из run(), враћајући false. Ово је стандардни начин заустављања RunLoop-а на позадинској нити. На Main Thread-у, CFRunLoopStop није препоручен — главни RunLoop треба да ради током целог живота апликације. За позадинске нити, након CFRunLoopStop нит може да се заврши или да настави извршавање следећег кода након run().
Често постављана питања
RunLoop — циклус догађаја у iOS-у имплементиран од стране CFRunLoop (Core Foundation) и NSRunLoop (Foundation). Чека догађаје (додири, тајмери, извори улаза) и прослеђује их руковаоцима на нити. Свака нит може имати један RunLoop, али се аутоматски креира само за Main Thread. RunLoop управља режимима (.default, .tracking, .common), изолујући обраду по приоритетима.
NSTimer се подразумевано додаје у .default режим RunLoop-а. Када корисник скролује, RunLoop прелази у .tracking режим и не обрађује тајмере из .default. Решење: додајте тајмер у .common режим преко RunLoop.current.add(timer, forMode: .common). .common обједињује .default и .tracking, па тајмер ради у оба режима.
Само ако позадинска нит користи тајмере (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream или Source догађаје. Ако нит извршава синхрони задатак (преузимање датотеке, израчунавања) и завршава — RunLoop није потребан. За покретање позовите RunLoop.current.run() након конфигурисања извора. За заустављање — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop ради на нивоу нити и обрађује догађаје секвенцијално, са подршком за режиме (modes). DispatchQueue — апстракција пула нити, задаци се извршавају на било којој слободној нити. GCD не подржава режиме и живи независно од RunLoop-а. DispatchQueue.main користи главни RunLoop за извршавање блокова — то је једина тачка пресека. За позадинске задатке пожељнији је GCD.
CADisplayLink — тајмер синхронизован са VSync (фреквенцијом освежавања екрана). Додаје се у RunLoop и окида пре сваког кадра рендеровања у фази BeforeTimers. CADisplayLink ради само на Main Thread-у, јер се рендеровање екрана одвија тамо. За непрекидне анимације при скроловању додајте га у .common режим: displayLink.add(to: .current, forMode: .common).
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође