iOS'ta RunLoop: nedir, çalışma modları ve olay döngüsü

Yazar: IT Sectr Yayınlanma: 2026-03-16 Okuma süresi: 11 dk

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 olayları işleyen event loop: dokunmalar, timer'lar, giriş kaynakları
  • Main Thread otomatik RunLoop'a sahiptir (CFRunLoopGetMain()), arka plan thread'leri — manuel başlatmak gerekir
  • Üç mod: .default (ana), .tracking (scroll), .common (default+tracking birleştirir)
  • NSTimer ve CADisplayLink, thread'de aktif RunLoop olmadan çalışmaz
  • RunLoop observers modlara giriş/çıkış ve işlemenin başlangıcı/bitişine tepki vermeyi sağlar

RunLoop Nedir

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 Nasıl Çalışır: Olay Döngüsünün Anatomisi

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.

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

RunLoop vs Looper (Android)

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 Modları: default, tracking, common

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.

ModCore Foundation SabitiFoundation SabitiNe zaman aktif
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultNormal durum, scroll yok
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingScroll, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonSözde mod: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeRunLoop'un ilk çalıştırılması

NSTimer Neden Scroll Sırasında Çalışmaz

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'lerinde RunLoop

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ı.

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

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

RunLoop Observer'ları: Döngü Olaylarını İzleme

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 ve Döngünün Sonlandırılması

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

iOS'ta RunLoop nedir?

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 neden scroll sırasında çalışmaz?

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.

Arka plan thread'inde RunLoop başlatmak gerekli mi?

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 ile GCD DispatchQueue arasındaki fark nedir?

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 RunLoop ile nasıl ilişkilidir?

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

  • RunLoop — iOS event loop: thread'de dokunmaları, timer'ları, giriş kaynaklarını işler; Main Thread otomatik RunLoop'a sahiptir
  • Üç mod: .default (genel), .tracking (scroll), .common (default + tracking) — olay filtrelemesini yönetir
  • .default'taki NSTimer scroll sırasında çalışmaz — çözüm: RunLoop.current.add(timer, forMode: .common) ile .common moduna ekleyin
  • Arka plan thread'leri varsayılan olarak RunLoop'a sahip değildir — timer'lar ve performSelector: için RunLoop.current.run() ile manuel başlatma gerekir
  • CADisplayLink — her VSync karesinde bir timer, akıcı animasyonlar için zorunludur; scroll sırasında çalışması için .common'a eklenir
  • RunLoop Observer — aşamaların izlenmesi: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; profiling için kullanılır
  • RunLoop ≠ Looper: iOS RunLoop modları ve timer'ları destekler, Android Looper daha basittir — mod yok, Handler + MessageQueue

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.

Projeyi tartış

Ayrıca okuyun