iOS-da RunLoop: nədir, iş rejimləri və hadisə dövrü

Müəllif: IT Sectr Dərc olunub: 2026-03-16 Oxuma vaxtı: 11 dəq

RunLoop — CFRunLoop (Core Foundation) və NSRunLoop (Foundation) obyektləri ilə reallaşdırılan iOS-da hadisə emal dövrü. Bu, hadisələri (toxunma, zamanlayıcılar, giriş mənbələri, bildirişlər) gözləyən və onları axında müvafiq işləyicilərə ötürən mexanizmdir. iOS-da hər bir axın maksimum bir RunLoop-a malikdir, lakin o, yalnız Main Thread üçün avtomatik yaradılır. Apple CFRunLoop Sənədlərinə görə, RunLoop fon axınlarında zamanlayıcıların, animasiyaların və mənbə monitorinqinin işi üçün vacibdir.

Əsas olanlar

  • RunLoop — axında hadisələri emal edən event loop: toxunma, zamanlayıcılar, giriş mənbələri
  • Main Thread avtomatik RunLoop-a malikdir (CFRunLoopGetMain()), fon axınları əl ilə işə salınmalıdır
  • Üç rejim: .default (əsas), .tracking (sürüşdürmə), .common (default+tracking birləşdirir)
  • NSTimer və CADisplayLink axında aktiv RunLoop olmadan işləmir
  • RunLoop observers rejimlərə giriş/çıxışa və emalın başlanğıcını/sonuna reaksiya verməyə imkan verir

RunLoop nədir

RunLoop — axında hadisələrin emalını təşkil edən Core Foundation infrastruktur obyektidir. Mahiyyətcə bu, hadisələrin (sources) gəlməsini gözləyən və onları işləyicilərə ötürən sonsuz dövrdür (while true). Hadisələr olmadıqda, RunLoop batareya enerjisinə qənaət edərək axını yuxu rejiminə (sleep) keçirir. Hadisə gəldikdə, axın oyanır, onu emal edir və yenidən yuxuya gedir. RunLoop yalnız iOS/macOS (XNU + Core Foundation) — Android-də onun rolunu Looper oynayır.

Hər bir axın ən çoxu bir RunLoop-a malikdir və o, ilk müraciətdə tənbəlcəsinə (lazy) yaradılır. Main Thread üçün RunLoop tətbiq işə salındıqda avtomatik yaradılır. Fon axınları üçün RunLoop CFRunLoopGetCurrent() və ya RunLoop.current çağrılmayana qədər yaradılmır. Tətbiqin əsas RunLoop-u toxunma hadisələrinin emalı, ekran renderinqi, DispatchQueue.main bloklarının icrası və Core Animation təbəqələrinə xidmət üçün cavabdehdir.

RunLoop axın deyil — bu, axın daxilindəki mexanizmdir. Axın RunLoop olmadan mövcud ola bilər (sinxron tapşırığı yerinə yetirib bitirirsə), lakin RunLoop axın olmadan mövcud ola bilməz. Aktiv RunLoop-u olan axında hadisələr olmadıqda, CPU bloklanmır, əksinə gözləmə vəziyyətindədir (waiting) — bu, 100% CPU sərf edən busy-wait dövründən əsas fərqdir.

RunLoop necə işləyir: hadisə dövrünün anatomiyası

RunLoop iki növ hadisə mənbəyini emal edir: Input Sources (giriş mənbələri) və Timer Sources (zamanlayıcılar). Input Sources asinxron hadisələri çatdırır: toxunma, siçan hərəkətləri, soketdən məlumatlar, digər axınlardan mesajlar (performSelector:onThread:). Timer Sources cədvəl üzrə sinxron hadisələri çatdırır: NSTimer, CADisplayLink. Həmçinin RunLoop vəziyyətini izləmək üçün Observers — giriş nöqtələri də var.

RunLoop dövürü ardıcıl fazalardan ibarətdir: rejimə giriş (kCFRunLoopEntry), zamanlayıcıların emalı (kCFRunLoopBeforeTimers), giriş mənbələrinin emalı (kCFRunLoopBeforeSources), mənbələrin emalı (kCFRunLoopAfterWaiting), gözləmə (sleep), rejimdən çıxış (kCFRunLoopExit). Əgər cari iterasiyada heç bir hadisə emal edilməyibsə, RunLoop yeni hadisə ilə oyanana qədər axını qeyri-müəyyən müddətə yuxuya göndərir.

swift
import Foundation

// RunLoop fazalarının Observer vasitəsilə nümayişi
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 aktivləşdi")
        case .beforeTimers:
            print("BeforeTimers — zamanlayıcıların emalı")
        case .beforeSources:
            print("BeforeSources — mənbələrin emalı")
        case .afterWaiting:
            print("AfterWaiting — yuxudan oyanma")
        case .exit:
            print("Exit — RunLoop tamamlandı")
        default:
            break
        }
    }

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

// Nümunə: RunLoop əsas axında zamanlayıcını emal edir
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // RunLoop.current.run() Main Thread-də UIApplicationMain tərəfindən çağrılır
    // avtomatik — əl ilə işə salmaq lazım deyil
    RunLoop.current.run() // Bu çağrı Main Thread-ə qayıtmayacaq
}

observeRunLoopActivities nümunəsi əsas RunLoop-da dövrün hər mərhələsini qeydə alan Observer qeydiyyatdan keçirir. Bu, debug üçün faydalıdır: .beforeTimers və .afterWaiting arasında uzun bir fasilə görürsünüzsə, RunLoop Main Thread-də bir əməliyyat tərəfindən bloklandığı anlamına gəlir. timerOnMainRunLoop NSTimer-in əsas RunLoop-da avtomatik olaraq necə işlədiyini göstərir — Timer.scheduledTimer yaradılarkən zamanlayıcı cari RunLoop-a defolt olaraq (.default mode) əlavə edilir.

RunLoop vs Looper (Android)

Android Looper — RunLoop-un analoqudur. Looper.prepare() axında mesaj növbəsi (MessageQueue) yaradır, Looper.loop() sonsuz emal dövrünü işə salır. Handler mesajları və Runnable-ları bu növbəyə göndərir. Əsas fərq: RunLoop rejimləri (modes) dəstəkləyir, Android Looper isə yox. Looper bütün mesajları rejim üzrə filtrləmədən emal edir ki, bu da onu daha sadə, lakin prioritet ssenarilərində daha az çevik edir (məsələn, iOS-da sürüşdürmə .tracking rejimində digər hadisələrdən ayrı emal olunur).

RunLoop Rejimləri: default, tracking, common

RunLoop Mode — hazırda aktiv olan mənbələr, zamanlayıcılar və müşahidəçilər toplusudur. Rejimlər hadisələrin emalını prioritetlərə görə təcrid etməyə imkan verir. İstifadəçi UITableView-i sürüşdürəndə, RunLoop .tracking rejiminə keçir, burada yalnız sürüşdürmə hadisələri və müvafiq zamanlayıcılar/animasiyalar emal olunur. Bütün digər mənbələr (məsələn, NSURLConnection) sürüşdürmə rejimindən çıxışa qədər dayandırılır.

Üç əsas rejim: .default (NSDefaultRunLoopMode) — sürüşdürmədən başqa bütün hadisələrin emal olunduğu əsas rejim; .tracking (UITrackingRunLoopMode) — sürüşdürmə və ya jest naviqasiyası zamanı aktivləşir; .common (NSRunLoopCommonModes) — ayrıca rejim deyil, .default + .tracking-i əhatə edən aliaslar toplusudur. Mənbənin .commonModes-a əlavə edilməsi onu avtomatik olaraq dəstin bütün rejimlərinə əlavə edir.

RejimCore Foundation sabitiFoundation sabitiNə vaxt aktivdir
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultAdi vəziyyət, sürüşdürməsiz
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingSürüşdürmə, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPseudo-rejim: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeRunLoop-un ilk işə salınması

NSTimer nə üçün sürüşdürmə zamanı işləmir

Klassik problem: NSTimer .default rejiminə əlavə edildikdə sürüşdürmə zamanı işləməyi dayandırır, çünki RunLoop .tracking rejiminə keçir və .default-dan zamanlayıcıları emal etmir. Həll yolu — zamanlayıcını .commonModes-a əlavə edin: RunLoop.current.add(timer, forMode: .common). Bu, zamanlayıcının həm .default, həm də .tracking rejimində işləməsinə səbəb olacaq. Alternativ NSTimer əvəzinə DispatchQueue.main.async istifadə etməkdir, çünki GCD axın səviyyəsində işləyir, RunLoop rejimlərində yox.

Fon axınlarında RunLoop

Fon axınları defolt olaraq RunLoop-a malik deyil. Fon axınında NSTimer işə salmaq, NSInputStream/NSOutputStream emal etmək və ya performSelector:-a reaksiya vermək lazımdırsa, RunLoop-u əl ilə yaratmalı və işə salmalısınız. RunLoop olmadan zamanlayıcı və performSelector: heç vaxt işləməyəcək — axın kodu işə salıb hadisələri gözləmədən bitirəcək.

Fon axınında RunLoop yaratmaq üçün axının sonunda RunLoop.current.run() çağırmaq kifayətdir. Bu çağrı axını qeyri-müəyyən müddətə bloklayır və hadisələri emal edir. Dayandırmaq üçün CFRunLoopStop(CFRunLoopGetCurrent()) istifadə edin. Vacibdir: RunLoop.current ilk müraciətdə tənbəlcəsinə RunLoop yaradır — əgər run() çağırmasanız, hadisələri emal etməyəcək. Nümunə: mənbələrin konfiqurasiyası -> RunLoop-a əlavə -> run() çağrılması.

swift
import Foundation

// Öz RunLoop-u olan fon axını
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // RunLoop.current çağrıldıqda RunLoop avtomatik yaradılır
            let runLoop = RunLoop.current

            // RunLoop-u aktiv saxlamaq üçün port əlavə edirik
            runLoop.add(Port(), forMode: .default)

            // Hadisə emalını işə salırıq
            var isFinished = false
            while !isFinished {
                // run(mode:before:) hadisə emal edildikdə true qaytarır
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

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

    func stop() {
        // Fon axınında RunLoop-un dayandırılması
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

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

// İstifadə: fon RunLoop-da zamanlayıcı
let manager = BackgroundRunLoopManager()
manager.start()

// Tapşırığın performSelector vasitəsilə fon RunLoop-a göndərilməsi
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager daimi RunLoop ilə fon axını yaradır. Boş Port() əlavə etmək RunLoop-un dərhal bitməməsi üçün zəruridir — mənbələr olmadan RunLoop.run() false qaytarır və çıxır. performSelector:onThread: fon RunLoop-a mesaj göndərir — RunLoop BeforeSources fazasına daxil olduqda emal olunacaq. Stop fon axınında CFRunLoopStop çağıraraq dövri dayandırır.

NSTimer RunLoop-un BeforeTimers fazasında emal etdiyi zamanlayıcı hadisəsi yaradır. Zamanlayıcılar repeating (təkrarlanan) və non-repeating (birdəfəlik) ola bilər. NSTimer dəqiqlik zəmanət vermir: əgər RunLoop uzun əməliyyatla bloklanıbsa, zamanlayıcı bloklama aradan qaldırıldıqdan sonra işləyəcək və bütün buraxılmış işləmələr birində birləşdiriləcək (təkrarlanan zamanlayıcı üçün — birdən çox olmayan „yetib“ işləmə).

CADisplayLink — ekran yenilənmə tezliyi ilə sinxronlaşdırılmış xüsusi zamanlayıcı (60/120/144 Hz). Animasiyalar və video yeniləmələr üçün istifadə olunur. CADisplayLink RunLoop-a əlavə edilir və hər render çərçivəsindən əvvəl (Core Animation təbəqəni renderə göndərməmişdən əvvəl) işləyir. Əgər çərçivə buraxılıbsa (display link 16 ms ərzində işləyə bilməyibsə), növbəti çağrı növbəti VSync dövründə baş verir.

swift
import UIKit

class AnimationController {

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

    // CADisplayLink — vsync ilə animasiya
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // .common rejiminə əlavə — sürüşdürmə zamanı da işləyir
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // Hər çərçivədə çağrılır (60 FPS → hər 16.6 ms)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // 5 saniyədən sonra dayanma
        }
    }

    // NSTimer — dövri tapşırıq
    func startTimerInCommonMode() {
        displayLinkTimer?.invalidate()
        displayLinkTimer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: true
        ) { [weak self] timer in
            print("Timer tick")
        }

        // ACAR: .common-a əlavə edirik, əks halda zamanlayıcı sürüşdürmədə donar
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

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

AnimationController-də CADisplayLink .common rejiminə əlavə edilib ki, bu da sürüşdürmədən asılı olmayaraq hər çərçivədə step çağrılmasına zəmanət verir. displayLink.add(to: .current, forMode: .common) — sürüşdürmə zamanı kəsilməməli olan animasiyalar üçün standart nümunədir. NSTimer də sürüşdürmə zamanı işləməsi üçün .common rejiminə əlavə edilib. Bu olmadan zamanlayıcı yalnız .default rejimində işləyəcəkdi.

RunLoop Observers: dövr hadisələrinin monitorinqi

CFRunLoopObserver — RunLoop fazalarını izləmək mexanizmi. Observer vasitəsilə rejimə giriş, zamanlayıcıların emalının başlanğıcı, mənbələrin emalının başlanğıcı, yuxudan oyanma, rejimdən çıxış barədə bildirişlər əldə etmək olar. Observer-lər frameworklər tərəfindən öz ehtiyacları üçün istifadə olunur: Core Animation onları RunLoop yuxusundan əvvəl təbəqələri render etmək, UIKit — hadisələri emal etdikdən sonra layout-u yeniləmək üçün istifadə edir.

Tərtibatçı öz məqsədləri üçün də Observer-lər əlavə edə bilər. Məsələn: hadisə emalı vaxtının ölçülməsi (profilləşdirmə), RunLoop yuxusundan əvvəl təxirə salınmış əməliyyatların icrası (UI artıq yenilənib və istifadəçi qarşılıqlı əlaqədə deyil), uzun müddətli hərəkətsizlik zamanı avtomatik məlumat yaddaşı. Observer CFRunLoopAddObserver vasitəsilə rejim və izlənən fəaliyyətlərin bit maskası ilə qeydiyyatdan keçirilir.

Observer üçün ən faydalı nöqtələr: .afterWaiting — RunLoop oyandıqdan sonra icra olunur və hadisə emal edildikdən sonra işləməli olan kodu ehtiva edə bilər; .beforeTimers — zamanlayıcıların emalından əvvəl, əvvəlki emaldan keçən vaxtı ölçməyə imkan verir; .exit — RunLoop dayandırıldıqda işlənir, fon axınının resurslarını təmizləmək üçün faydalıdır.

CFRunLoopStop və dövrün tamamlanması

CFRunLoopStop — cari RunLoop iterasiyasını məcburi bitirən funksiyadır. CFRunLoopStop(CFRunLoopGetCurrent()) çağrıldıqda RunLoop cari hadisənin emalını bitirir və run()-dan false qaytarmaqla çıxır. Bu, fon axınında RunLoop-u dayandırmaq üçün standart üsuldur. Main Thread-də CFRunLoopStop tövsiyə olunmur — əsas RunLoop tətbiqin bütün ömrü boyu işləməlidir. Fon axınları üçün CFRunLoopStop-dan sonra axın bitə bilər və ya run()-dan sonra növbəti kodu icra etməyə davam edə bilər.

Tez-tez verilən suallar

iOS-da RunLoop nədir?

RunLoop — CFRunLoop (Core Foundation) və NSRunLoop (Foundation) tərəfindən həyata keçirilən iOS-da hadisə emal dövrüdür. Hadisələri (toxunma, zamanlayıcılar, giriş mənbələri) gözləyir və onları axında işləyicilərə ötürür. Hər bir axın bir RunLoop-a malik ola bilər, lakin avtomatik olaraq yalnız Main Thread üçün yaradılır. RunLoop rejimləri (.default, .tracking, .common) idarə edir, emalı prioritetlərə görə təcrid edir.

NSTimer nə üçün sürüşdürmə zamanı işləmir?

NSTimer defolt olaraq RunLoop-un .default rejiminə əlavə edilir. İstifadəçi sürüşdürəndə RunLoop .tracking rejiminə keçir və .default-dan zamanlayıcıları emal etmir. Həll: zamanlayıcını .common rejiminə əlavə edin: RunLoop.current.add(timer, forMode: .common). .common .default və .tracking-i birləşdirir, buna görə zamanlayıcı hər iki rejimdə işləyir.

Fon axınında RunLoop işə salmaq lazımdırmı?

Yalnız fon axını zamanlayıcılardan (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream və ya Source hadisələrindən istifadə edirsə. Əgər axın sinxron tapşırığı (fayl yükləmə, hesablama) yerinə yetirib bitirirsə — RunLoop lazım deyil. İşə salmaq üçün mənbələri konfiqurasiya etdikdən sonra RunLoop.current.run() çağırın. Dayandırmaq üçün — CFRunLoopStop(CFRunLoopGetCurrent()).

RunLoop GCD DispatchQueue-dən nə ilə fərqlənir?

RunLoop axın səviyyəsində işləyir və hadisələri rejimlər (modes) dəstəkləyərək ardıcıl emal edir. DispatchQueue — axın hovuzunun abstraksiyasıdır, tapşırıqlar istənilən boş axında icra olunur. GCD rejimləri dəstəkləmir və RunLoop-dan asılı olmayaraq mövcuddur. DispatchQueue.main blokları icra etmək üçün əsas RunLoop-dan istifadə edir — bu yeganə kəsişmə nöqtəsidir. Fon tapşırıqları üçün GCD üstünlük təşkil edir.

CADisplayLink RunLoop ilə necə əlaqəlidir?

CADisplayLink — VSync (ekran yenilənmə tezliyi) ilə sinxronlaşdırılmış zamanlayıcıdır. RunLoop-a əlavə edilir və BeforeTimers fazasında hər render çərçivəsindən əvvəl işləyir. CADisplayLink yalnız Main Thread-də işləyir, çünki ekran renderinqi orada baş verir. Sürüşdürmə zamanı fasiləsiz animasiyalar üçün onu .common rejiminə əlavə edin: displayLink.add(to: .current, forMode: .common).

Nəticələr

  • RunLoop — iOS hadisə dövrü: axında toxunma, zamanlayıcılar, giriş mənbələrini emal edir; Main Thread avtomatik RunLoop-a malikdir
  • Üç rejim: .default (ümumi), .tracking (sürüşdürmə), .common (default + tracking) — hadisə filtrləməsini idarə edir
  • .default-da NSTimer sürüşdürmə zamanı işləmir — həll: RunLoop.current.add(timer, forMode: .common) ilə .common rejiminə əlavə etmək
  • Fon axınları defolt olaraq RunLoop-a malik deyil — zamanlayıcılar və performSelector: üçün RunLoop.current.run() ilə əl ilə işə salma tələb olunur
  • CADisplayLink — hər VSync çərçivəsi üçün zamanlayıcı, hamar animasiyalar üçün vacibdir; sürüşdürmə zamanı işləmək üçün .common-a əlavə edilir
  • RunLoop Observer — fazaların monitorinqi: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; profilləşdirmə üçün istifadə olunur
  • RunLoop ≠ Looper: iOS RunLoop rejimləri və zamanlayıcıları dəstəkləyir, Android Looper daha sadədir — rejimlər yoxdur, Handler + MessageQueue

Açar təslim mobil tətbiq hazırlayacağıq

IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.

Layihəni müzakirə et

Həm də oxuyun