RunLoop — iOS'ta CFRunLoop (Core Foundation) ve NSRunLoop (Foundation) nesneleri tarafından gerçekleştirilen bir olay işleme döngüsüdür. Olayları (dokunmalar, timer'lar, giriş kaynakları, bildirimler) bekleyen ve bunları thread'deki ilgili işleyicilere gönderen bir mekanizmadır. iOS'ta her thread en fazla bir RunLoop'a sahiptir, ancak otomatik olarak yalnızca Main Thread için oluşturulur. Apple CFRunLoop Documentation'a göre RunLoop, arka plan thread'lerinde timer'ların, animasyonların ve kaynak izlemenin çalışması için kritiktir.
Önemli noktalar
RunLoop — thread'de olay işlemeyi düzenleyen bir Core Foundation altyapı nesnesidir. Özünde, olayların (sources) gelmesini bekleyen ve bunları işleyicilere ileten sonsuz bir döngüdür (while true). Olay olmadığında, RunLoop thread'i uyku moduna (sleep) geçirerek pil gücünden tasarruf sağlar. Bir olay geldiğinde thread uyanır, olayı işler ve tekrar uyur. RunLoop yalnızca iOS/macOS'ta (XNU + Core Foundation) bulunur — Android'de bu rolü Looper üstlenir.
Her thread en fazla bir RunLoop'a sahiptir ve bu RunLoop ilk erişimde tembel (lazy) olarak oluşturulur. Main Thread için RunLoop, uygulama başlatıldığında otomatik olarak oluşturulur. Arka plan thread'leri için CFRunLoopGetCurrent() veya RunLoop.current çağrılana kadar RunLoop oluşturulmaz. Ana uygulama RunLoop'u, dokunma olaylarını işleme, ekran render'ı, DispatchQueue.main bloklarını yürütme ve Core Animation katmanlarını yönetmeden sorumludur.
RunLoop bir thread değildir — thread'in içindeki bir mekanizmadır. Bir thread RunLoop olmadan var olabilir (senkron bir görevi yürütüp sonlanırsa), ancak RunLoop thread olmadan var olamaz. Aktif RunLoop'a sahip bir thread'de olay yoksa, CPU'yu bloke etmez, bekleme durumunda (waiting) kalır — bu, %100 CPU tüketen busy-wait döngüsünden temel farktır.
RunLoop iki tür olay kaynağını işler: Input Sources (giriş kaynakları) ve Timer Sources (zamanlayıcılar). Input Sources asenkron olayları iletir: dokunmalar, fare hareketleri, socket verileri, diğer thread'lerden mesajlar (performSelector:onThread:). Timer Sources zamanlanmış senkron olayları iletir: NSTimer, CADisplayLink. Ayrıca RunLoop durumunu izlemek için giriş noktaları olan Observer'lar da bulunur.
RunLoop döngüsü sıralı aşamalardan oluşur: moda giriş (kCFRunLoopEntry), timer'ların işlenmesi (kCFRunLoopBeforeTimers), giriş kaynaklarının işlenmesi (kCFRunLoopBeforeSources), kaynakların işlenmesi (kCFRunLoopAfterWaiting), bekleme (sleep), moddan çıkış (kCFRunLoopExit). Geçerli yinelemede hiçbir olay işlenmezse, RunLoop thread'i yeni bir olayla uyandırılana kadar belirsiz bir süre uykuya alır.
import Foundation
// RunLoop aşamalarının Observer aracılığıyla gösterimi
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 etkinleştirildi")
case .beforeTimers:
print("BeforeTimers — timer'ların işlenmesi")
case .beforeSources:
print("BeforeSources — kaynakların işlenmesi")
case .afterWaiting:
print("AfterWaiting — uykudan uyanma")
case .exit:
print("Exit — RunLoop sonlandırıldı")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Örnek: RunLoop ana thread'de bir timer'ı işler
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// Main Thread'de RunLoop.current.run() UIApplicationMain tarafından çağrılır
// otomatik olarak — manuel başlatmaya gerek yok
RunLoop.current.run() // Bu çağrı Main Thread'e geri dönmez
}
observeRunLoopActivities örneği, ana RunLoop'a döngünün her aşamasını kaydeden bir Observer kaydeder. Bu, hata ayıklama için yararlıdır: .beforeTimers ve .afterWaiting arasında uzun bir boşluk görürseniz, RunLoop'un Main Thread'de bir işlem tarafından bloke edildiği anlamına gelir. timerOnMainRunLoop, NSTimer'in ana RunLoop'da otomatik olarak nasıl çalıştığını gösterir — Timer.scheduledTimer oluşturulduğunda, timer geçerli RunLoop'a varsayılan modda (.default mode) eklenir.
Android Looper — RunLoop'un benzeridir. Looper.prepare() thread'de bir mesaj kuyruğu (MessageQueue) oluşturur, Looper.loop() sonsuz işleme döngüsünü başlatır. Handler bu kuyruğa mesajlar ve Runnable'lar gönderir. Temel fark: RunLoop modları (modes) desteklerken, Android Looper desteklemez. Looper tüm mesajları mod filtresi olmadan işler, bu da onu daha basit ancak öncelikli senaryolar için daha az esnek yapar (örneğin, iOS'ta scroll .tracking modunda diğer olaylardan ayrı olarak işlenir).
RunLoop Mode — o anda aktif olan kaynaklar, timer'lar ve gözlemciler kümesidir. Modlar, olay işlemeyi önceliklere göre izole etmeye olanak tanır. Kullanıcı bir UITableView'de scroll yaptığında, RunLoop .tracking moduna geçer ve bu modda yalnızca scroll olayları ile ilgili timer'lar/animasyonlar işlenir. Diğer tüm kaynaklar (örneğin, NSURLConnection) scroll modundan çıkana kadar duraklatılır.
Üç ana mod: .default (NSDefaultRunLoopMode) — scroll dışındaki tüm olayların işlendiği ana mod; .tracking (UITrackingRunLoopMode) — scroll veya jest navigasyonu sırasında etkinleşir; .common (NSRunLoopCommonModes) — ayrı bir mod değil, .default + .tracking'i içeren bir takma ad kümesidir. Bir kaynağı .commonModes'a eklemek, onu otomatik olarak kümedeki tüm modlara ekler.
| Mod | Core Foundation Sabiti | Foundation Sabiti | Ne zaman aktif |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Normal durum, scroll yok |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Scroll, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Sözde mod: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | RunLoop'un ilk çalıştırılması |
Klasik sorun: .default moduna eklenen NSTimer, scroll sırasında çalışmayı durdurur çünkü RunLoop .tracking moduna geçer ve .default'daki timer'ları işlemez. Çözüm — timer'ı .commonModes'a eklemek: RunLoop.current.add(timer, forMode: .common). Bu, Timer'ın hem .default hem de .tracking'de çalışmasını sağlar. Alternatif — NSTimer yerine DispatchQueue.main.async kullanmak, çünkü GCD thread seviyesinde çalışır, RunLoop modlarında değil.
Arka plan thread'leri varsayılan olarak RunLoop'a sahip değildir. Bir arka plan thread'inde NSTimer çalıştırmak, NSInputStream/NSOutputStream işlemek veya performSelector:'a tepki vermek gerekiyorsa, RunLoop'u manuel olarak oluşturmak ve başlatmak gerekir. RunLoop olmadan timer ve performSelector: asla çalışmaz — thread kodu yürütür ve olayları beklemeden sonlanır.
Bir arka plan thread'inde RunLoop oluşturmak için thread'in çalışmasının sonunda RunLoop.current.run() çağırmak yeterlidir. Bu çağrı thread'i süresiz olarak bloke eder ve olayları işler. Durdurmak için CFRunLoopStop(CFRunLoopGetCurrent()) kullanın. Önemli: RunLoop.current, RunLoop'u ilk çağrıda tembel olarak oluşturur — run() çağrılmazsa olayları işlemez. Desen: kaynakların yapılandırılması → RunLoop'a ekleme → run() çağrısı.
import Foundation
// Kendi RunLoop'una sahip arka plan thread'i
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop, RunLoop.current çağrıldığında otomatik olarak oluşturulur
let runLoop = RunLoop.current
// RunLoop'u aktif tutmak için boş bir Port ekliyoruz
runLoop.add(Port(), forMode: .default)
// Olay işlemeyi başlatıyoruz
var isFinished = false
while !isFinished {
// run(mode:before:), bir olay işlendiyse true döndürür
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Arka plan thread'inde RunLoop'un durdurulması
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Kullanım: arka plan RunLoop'unda timer
let manager = BackgroundRunLoopManager()
manager.start()
// performSelector aracılığıyla arka plan RunLoop'una görev gönderme
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager, kalıcı RunLoop'a sahip bir arka plan thread'i oluşturur. Boş bir Port() eklenmesi, RunLoop'un hemen sonlanmaması için gereklidir — kaynak olmadan RunLoop.run() false döndürür ve çıkar. performSelector:onThread:, arka plan RunLoop'una bir mesaj gönderir — RunLoop BeforeSources aşamasına girdiğinde işlenir. Stop, arka plan thread'inde CFRunLoopStop çağırarak döngüyü sonlandırır.
NSTimer, RunLoop'un BeforeTimers aşamasında işlediği bir timer olayı oluşturur. Timer'lar repeating (tekrarlayan) ve non-repeating (tek seferlik) olabilir. NSTimer hassasiyet garanti etmez: RunLoop uzun bir işlem tarafından bloke edilirse, timer blokaj kalktıktan sonra çalışır ve kaçırılan tüm çalıştırmalar tek bir çalıştırmada birleşir (repeating timer'lar için — en fazla bir “yakalanan” çalıştırma).
CADisplayLink — ekran yenileme hızıyla (60/120/144 Hz) senkronize özel bir timer'dır. Animasyonlar ve video güncellemesi için kullanılır. CADisplayLink RunLoop'a eklenir ve her render karesinden önce (Core Animation katmanı render'a göndermeden önce) çalışır. Bir kare kaçırılırsa (display link 16 ms'de çalışamazsa), sonraki çağrı bir sonraki VSync döngüsünde gerçekleşir.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — vsync ile animasyon
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// .common moduna ekleme — scroll sırasında da çalışır
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Her karede çağrılır (60 FPS → her 16.6 ms'de bir)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // 5 saniye sonra durdurma
}
}
// NSTimer — periyodik görev
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// ANAHTAR: .common'a ekliyoruz, aksi halde timer scroll'da donar
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
AnimationController'da CADisplayLink .common moduna eklenir, bu da step'in scroll'dan bağımsız olarak her karede çağrılmasını garanti eder. displayLink.add(to: .current, forMode: .common) — scroll sırasında kesintiye uğramaması gereken animasyonlar için standart desendir. NSTimer da scroll sırasında çalışması için .common moduna eklenir. Bu olmadan timer yalnızca .default modunda çalışırdı.
CFRunLoopObserver — RunLoop aşamalarını izlemek için bir mekanizmadır. Observer ile moda giriş, timer işlemenin başlangıcı, kaynak işlemenin başlangıcı, uykudan uyanma, moddan çıkış hakkında bildirimler alınabilir. Observer'lar framework'ler tarafından kendi ihtiyaçları için kullanılır: Core Animation, RunLoop uyumadan önce katmanları render etmek için; UIKit, olay işlemeden sonra layout'u güncellemek için kullanır.
Geliştirici de kendi amaçları için Observer'lar ekleyebilir. Örneğin: olay işleme süresini ölçme (profiling), RunLoop uyumadan önce ertelenmiş işlemleri yürütme (UI güncellendiğinde ve kullanıcı etkileşimde değilken), uzun süreli hareketsizlikte otomatik veri kaydetme. Observer, CFRunLoopAddObserver aracılığıyla mod ve izlenecek aktivitelerin bit maskesi belirtilerek kaydedilir.
Observer için en kullanışlı noktalar: .afterWaiting — RunLoop uyandıktan sonra yürütülür ve olay işlendikten sonra çalışması gereken kodu içerebilir; .beforeTimers — timer'ların işlenmesinden önce, önceki işlemeden bu yana geçen süreyi ölçmeye olanak tanır; .exit — RunLoop durduğunda çalışır, arka plan thread'inin kaynaklarını temizlemek için kullanışlıdır.
CFRunLoopStop — RunLoop'un geçerli yinelemesini zorla sonlandıran fonksiyondur. CFRunLoopStop(CFRunLoopGetCurrent()) çağrıldığında, RunLoop geçerli olayın işlenmesini tamamlar ve run()'dan false döndürerek çıkar. Bu, arka plan thread'inde RunLoop'u durdurmanın standart yoludur. Main Thread'de CFRunLoopStop önerilmez — ana RunLoop uygulama ömrü boyunca çalışmalıdır. Arka plan thread'leri için CFRunLoopStop'tan sonra thread sonlanabilir veya run()'dan sonraki kodu yürütmeye devam edebilir.
Sıkça Sorulan Sorular
RunLoop — iOS'ta CFRunLoop (Core Foundation) ve NSRunLoop (Foundation) tarafından gerçekleştirilen olay işleme döngüsüdür. Olayları (dokunmalar, timer'lar, giriş kaynakları) bekler ve bunları thread'deki işleyicilere gönderir. Her thread bir RunLoop'a sahip olabilir, ancak otomatik olarak yalnızca Main Thread için oluşturulur. RunLoop, işlemeyi önceliklere göre izole ederek modları (.default, .tracking, .common) yönetir.
NSTimer varsayılan olarak RunLoop'un .default moduna eklenir. Kullanıcı scroll yaptığında, RunLoop .tracking moduna geçer ve .default'daki timer'ları işlemez. Çözüm: timer'ı RunLoop.current.add(timer, forMode: .common) ile .common moduna ekleyin. .common, .default ve .tracking'i birleştirir, bu nedenle timer her iki modda da çalışır.
Yalnızca arka plan thread'i timer'lar (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream veya Source olayları kullanıyorsa. Thread senkron bir görev yürütüyorsa (dosya indirme, hesaplama) ve sonlanıyorsa — RunLoop gerekmez. Başlatmak için kaynakları yapılandırdıktan sonra RunLoop.current.run() çağırın. Durdurmak için — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop thread seviyesinde çalışır ve olayları modlar desteğiyle sıralı olarak işler. DispatchQueue — thread havuzu soyutlamasıdır, görevler herhangi bir boş thread'de yürütülür. GCD modları desteklemez ve RunLoop'dan bağımsız yaşar. DispatchQueue.main, blokları yürütmek için ana RunLoop'u kullanır — bu tek kesişme noktasıdır. Arka plan görevleri için GCD tercih edilir.
CADisplayLink — VSync (ekran yenileme hızı) ile senkronize bir timer'dır. RunLoop'a eklenir ve BeforeTimers aşamasında her render karesinden önce çalışır. CADisplayLink yalnızca Main Thread'de çalışır, çünkü ekran render'ı orada gerçekleşir. Scroll sırasında kesintisiz animasyonlar için .common moduna ekleyin: displayLink.add(to: .current, forMode: .common).
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun