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ə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 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.
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.
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 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.
| Rejim | Core Foundation sabiti | Foundation sabiti | Nə vaxt aktivdir |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Adi vəziyyət, sürüşdürməsiz |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Sürüşdürmə, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-rejim: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | RunLoop-un ilk işə salınması |
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ı 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ı.
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.
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.
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 — 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
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 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.
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 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 — 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
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.
Həm də oxuyun