RunLoop у iOS-у: шта је то, режими рада и циклус догађаја

Аутор: IT Sectr Објављено: 2026-03-16 Време читања: 11 мин

RunLoop — циклус обраде догађаја у iOS-у, имплементиран од стране CFRunLoop (Core Foundation) и NSRunLoop (Foundation). Ово је механизам који чека догађаје (додири, тајмери, извори улаза, обавештења) и прослеђује их одговарајућим руковаоцима на нити. Свака нит у iOS-у има највише један RunLoop, али се он аутоматски креира само за Main Thread. Према Apple CFRunLoop документацији, RunLoop је критичан за рад тајмера, анимација и надгледање извора у позадинским нитима.

Главно

  • RunLoop — event loop који обрађује догађаје на нити: додири, тајмери, извори улаза
  • Main Thread има аутоматски RunLoop (CFRunLoopGetMain()), позадинске нити треба ручно покренути
  • Три режима: .default (главни), .tracking (скрол), .common (обједињује default+tracking)
  • NSTimer и CADisplayLink не раде без активног RunLoop на нити
  • RunLoop observers омогућавају реакцију на улаз/излаз из режима и почетак/крај обраде

Шта је 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: анатомија циклуса догађаја

RunLoop обрађује два типа извора догађаја: Input Sources (улазни извори) и Timer Sources (тајмери). Input Sources достављају асинхроне догађаје: додири, покрети миша, подаци из сокета, поруке од других нити (performSelector:onThread:). Timer Sources достављају синхроне догађаје по распореду: NSTimer, CADisplayLink. Такође постоје Observers — улазне тачке за надгледање стања RunLoop-а.

Циклус RunLoop-а се састоји од узастопних фаза: улазак у режим (kCFRunLoopEntry), обрада тајмера (kCFRunLoopBeforeTimers), обрада улазних извора (kCFRunLoopBeforeSources), обрада извора (kCFRunLoopAfterWaiting), чекање (sleep), излазак из режима (kCFRunLoopExit). Ако у тренутној итерацији није обрађен ниједан догађај, RunLoop успављује нит на неодређено време до буђења новим догађајем.

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

RunLoop vs Looper (Android)

Android Looper — аналог RunLoop-а. Looper.prepare() креира ред порука (MessageQueue) на нити, Looper.loop() покреће бесконачну петљу обраде. Handler шаље поруке и Runnable у овај ред. Главна разлика: RunLoop подржава режиме (modes), а Android Looper не. Looper обрађује све поруке без филтрирања по режиму, што га чини једноставнијим, али мање флексибилним за сценарије са приоритетима (нпр. скрол у iOS-у се обрађује у .tracking режиму одвојено од других догађаја).

RunLoop режими: default, tracking, common

RunLoop Mode — скуп извора, тајмера и посматрача који су активни у датом тренутку. Режими омогућавају изоловање обраде догађаја по приоритетима. Када корисник скролује UITableView, RunLoop прелази у .tracking режим, у коме се обрађују само догађаји скроловања и одговарајући тајмери/анимације. Сви остали извори (нпр. NSURLConnection) се заустављају до изласка из режима скроловања.

Три главна режима: .default (NSDefaultRunLoopMode) — главни режим у коме се обрађују сви догађаји осим скроловања; .tracking (UITrackingRunLoopMode) — активира се при скроловању или гестовној навигацији; .common (NSRunLoopCommonModes) — није посебан режим, већ скуп псеудонима који укључује .default + .tracking. Додавање извора у .commonModes аутоматски га додаје у све режиме скупа.

РежимCore Foundation константаFoundation константаКада је активан
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultНормално стање, без скроловања
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingСкроловање, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonПсеудо-режим: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeПрво покретање RunLoop-а

Зашто NSTimer не ради током скроловања

Класичан проблем: NSTimer додат у .default режим престаје да ради током скроловања, јер RunLoop прелази у .tracking режим и не обрађује тајмере из .default. Решење — додајте тајмер у .commonModes: RunLoop.current.add(timer, forMode: .common). То ће учинити да тајмер ради и у .default и у .tracking. Алтернатива је коришћење DispatchQueue.main.async уместо NSTimer, пошто GCD ради на нивоу нити, а не RunLoop режима.

RunLoop у позадинским нитима

Позадинске нити немају RunLoop подразумевано. Ако у позадинској нити треба покренути NSTimer, обрађивати NSInputStream/NSOutputStream или реаговати на performSelector:, потребно је ручно креирати и покренути RunLoop. Без RunLoop-а, тајмер и performSelector: никада неће радити — нит ће покренути код и завршити се без чекања на догађаје.

За креирање RunLoop-а у позадинској нити довољно је позвати RunLoop.current.run() на крају рада нити. Овај позив блокира нит на неодређено време, обрађујући догађаје. За заустављање користите CFRunLoopStop(CFRunLoopGetCurrent()). Важно: RunLoop.current креира RunLoop лењо при првом приступу — ако не позовете run(), неће обрађивати догађаје. Образац: конфигурација извора -> додавање у RunLoop -> позивање run().

swift
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 циклусу.

swift
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 режиму.

RunLoop Observers: надгледање циклуса догађаја

CFRunLoopObserver — механизам за праћење фаза RunLoop-а. Помоћу Observer-а можете добијати обавештења о уласку у режим, почетку обраде тајмера, почетку обраде извора, буђењу из сна, изласку из режима. Observer-и се користе од стране framework-а за своје потребе: Core Animation их користи за рендеровање слојева пре спавања RunLoop-а, UIKit — за ажурирање layout-а након обраде догађаја.

Програмер такође може додавати Observer-е за своје потребе. На пример: мерење времена обраде догађаја (профилисање), извршавање одложених операција пре спавања RunLoop-а (када је UI већ ажуриран и корисник не интерагује), аутоматско чување података при дугом неактивности. Observer се региструје кроз CFRunLoopAddObserver са навођењем режима и бит маске праћених активности.

Најкорисније тачке за Observer: .afterWaiting — извршава се након буђења RunLoop-а и може садржати код који треба да се покрене након обраде догађаја; .beforeTimers — пре обраде тајмера, омогућава мерење времена протеклог од претходне обраде; .exit — окида се при заустављању RunLoop-а, корисно за чишћење ресурса позадинске нити.

CFRunLoopStop и завршетак циклуса

CFRunLoopStop — функција која присилно завршава тренутну итерацију RunLoop-а. При позиву CFRunLoopStop(CFRunLoopGetCurrent()), RunLoop завршава обраду тренутног догађаја и излази из run(), враћајући false. Ово је стандардни начин заустављања RunLoop-а на позадинској нити. На Main Thread-у, CFRunLoopStop није препоручен — главни RunLoop треба да ради током целог живота апликације. За позадинске нити, након CFRunLoopStop нит може да се заврши или да настави извршавање следећег кода након run().

Често постављана питања

Шта је RunLoop у iOS-у?

RunLoop — циклус догађаја у iOS-у имплементиран од стране CFRunLoop (Core Foundation) и NSRunLoop (Foundation). Чека догађаје (додири, тајмери, извори улаза) и прослеђује их руковаоцима на нити. Свака нит може имати један RunLoop, али се аутоматски креира само за Main Thread. RunLoop управља режимима (.default, .tracking, .common), изолујући обраду по приоритетима.

Зашто NSTimer не ради током скроловања?

NSTimer се подразумевано додаје у .default режим RunLoop-а. Када корисник скролује, RunLoop прелази у .tracking режим и не обрађује тајмере из .default. Решење: додајте тајмер у .common режим преко RunLoop.current.add(timer, forMode: .common). .common обједињује .default и .tracking, па тајмер ради у оба режима.

Да ли треба покренути RunLoop у позадинској нити?

Само ако позадинска нит користи тајмере (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream или Source догађаје. Ако нит извршава синхрони задатак (преузимање датотеке, израчунавања) и завршава — RunLoop није потребан. За покретање позовите RunLoop.current.run() након конфигурисања извора. За заустављање — CFRunLoopStop(CFRunLoopGetCurrent()).

По чему се RunLoop разликује од GCD DispatchQueue?

RunLoop ради на нивоу нити и обрађује догађаје секвенцијално, са подршком за режиме (modes). DispatchQueue — апстракција пула нити, задаци се извршавају на било којој слободној нити. GCD не подржава режиме и живи независно од RunLoop-а. DispatchQueue.main користи главни RunLoop за извршавање блокова — то је једина тачка пресека. За позадинске задатке пожељнији је GCD.

Како је CADisplayLink повезан са RunLoop-ом?

CADisplayLink — тајмер синхронизован са VSync (фреквенцијом освежавања екрана). Додаје се у RunLoop и окида пре сваког кадра рендеровања у фази BeforeTimers. CADisplayLink ради само на Main Thread-у, јер се рендеровање екрана одвија тамо. За непрекидне анимације при скроловању додајте га у .common режим: displayLink.add(to: .current, forMode: .common).

Закључци

  • RunLoop — iOS event loop: обрађује додире, тајмере, изворе улаза на нити; Main Thread има аутоматски RunLoop
  • Три режима: .default (општи), .tracking (скрол), .common (default + tracking) — управљају филтрирањем догађаја
  • NSTimer у .default не ради при скроловању — решење: додавање у .common режим преко RunLoop.current.add(timer, forMode: .common)
  • Позадинске нити немају RunLoop подразумевано — за тајмере и performSelector: потребно ручно покретање преко RunLoop.current.run()
  • CADisplayLink — тајмер за сваки VSync кадар, неопходан за глатке анимације; додаје се у .common за рад при скроловању
  • RunLoop Observer — надгледање фаза: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; користи се за профилисање
  • RunLoop ≠ Looper: iOS RunLoop подржава режиме и тајмере, Android Looper је једноставнији — без режима, Handler + MessageQueue

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође