RunLoop di iOS: apa itu, mode kerja dan siklus acara

Penulis: IT Sectr Diterbitkan: 2026-03-16 Waktu membaca: 11 mnt

RunLoop — siklus pemrosesan acara di iOS, diimplementasikan oleh objek CFRunLoop (Core Foundation) dan NSRunLoop (Foundation). Ini adalah mekanisme yang menunggu acara (sentuhan, timer, sumber input, pemberitahuan) dan mengirimkannya ke handler yang sesuai pada thread. Setiap thread di iOS memiliki maksimal satu RunLoop, tetapi secara otomatis dibuat hanya untuk Main Thread. Menurut Dokumentasi Apple CFRunLoop, RunLoop sangat penting untuk kerja timer, animasi, dan pemantauan sumber di thread latar belakang.

Poin Penting

  • RunLoop — event loop yang memproses acara di thread: sentuhan, timer, sumber input
  • Main Thread memiliki RunLoop otomatis (CFRunLoopGetMain()), thread latar belakang perlu dijalankan manual
  • Tiga mode: .default (utama), .tracking (scroll), .common (menggabungkan default+tracking)
  • NSTimer dan CADisplayLink tidak bekerja tanpa RunLoop aktif di thread
  • RunLoop observers memungkinkan reaksi terhadap masuk/keluar mode dan awal/akhir pemrosesan

Apa itu RunLoop

RunLoop — adalah objek infrastruktur Core Foundation yang mengatur pemrosesan acara pada thread. Pada dasarnya ini adalah loop tak terbatas (while true) yang menunggu datangnya acara (sources) dan meneruskannya ke handler. Ketika tidak ada acara, RunLoop menidurkan thread (sleep), menghemat energi baterai. Saat acara datang, thread terbangun, memprosesnya, dan kembali tidur. RunLoop hanya ada di iOS/macOS (XNU + Core Foundation) — di Android perannya dijalankan oleh Looper.

Setiap thread memiliki paling banyak satu RunLoop, yang dibuat secara malas (lazy) pada akses pertama. Untuk Main Thread, RunLoop dibuat otomatis saat aplikasi dimulai. Untuk thread latar belakang, RunLoop tidak dibuat sampai CFRunLoopGetCurrent() atau RunLoop.current dipanggil. RunLoop utama aplikasi bertanggung jawab untuk memproses acara sentuhan, rendering layar, menjalankan blok DispatchQueue.main, dan melayani lapisan Core Animation.

RunLoop bukan thread — ini adalah mekanisme di dalam thread. Thread dapat ada tanpa RunLoop (jika menjalankan tugas sinkron dan selesai), tetapi RunLoop tidak dapat ada tanpa thread. Ketika thread dengan RunLoop aktif tidak memiliki acara, ia tidak memblokir CPU, melainkan dalam status menunggu (waiting) — ini adalah perbedaan utama dari busy-wait yang menghabiskan 100% CPU.

Bagaimana RunLoop Bekerja: anatomi siklus acara

RunLoop memproses dua jenis sumber acara: Input Sources (sumber input) dan Timer Sources (timer). Input Sources mengirimkan acara asinkron: sentuhan, gerakan mouse, data dari soket, pesan dari thread lain (performSelector:onThread:). Timer Sources mengirimkan acara sinkron sesuai jadwal: NSTimer, CADisplayLink. Juga ada Observers — titik masuk untuk memantau status RunLoop.

Siklus RunLoop terdiri dari fase berurutan: masuk ke mode (kCFRunLoopEntry), pemrosesan timer (kCFRunLoopBeforeTimers), pemrosesan sumber input (kCFRunLoopBeforeSources), pemrosesan sumber (kCFRunLoopAfterWaiting), menunggu (sleep), keluar dari mode (kCFRunLoopExit). Jika dalam iterasi saat ini tidak ada acara yang diproses, RunLoop menidurkan thread untuk waktu tidak terbatas hingga terbangun oleh acara baru.

swift
import Foundation

// Demonstrasi fase RunLoop melalui 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 diaktifkan")
        case .beforeTimers:
            print("BeforeTimers — pemrosesan timer")
        case .beforeSources:
            print("BeforeSources — pemrosesan sumber")
        case .afterWaiting:
            print("AfterWaiting — terbangun setelah tidur")
        case .exit:
            print("Exit — RunLoop selesai")
        default:
            break
        }
    }

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

// Contoh: RunLoop memproses timer di thread utama
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // RunLoop.current.run() di Main Thread dipanggil oleh UIApplicationMain
    // otomatis — tidak perlu dijalankan manual
    RunLoop.current.run() // Panggilan ini tidak akan kembali ke Main Thread
}

Contoh observeRunLoopActivities mendaftarkan Observer pada RunLoop utama yang mencatat setiap fase siklus. Ini berguna untuk debugging: jika Anda melihat interval panjang antara .beforeTimers dan .afterWaiting, berarti RunLoop diblokir oleh operasi di Main Thread. timerOnMainRunLoop menunjukkan bagaimana NSTimer bekerja secara otomatis pada RunLoop utama — saat membuat Timer.scheduledTimer, timer ditambahkan ke RunLoop saat ini secara default (.default mode).

RunLoop vs Looper (Android)

Android Looper — analog dari RunLoop. Looper.prepare() membuat antrian pesan (MessageQueue) pada thread, Looper.loop() memulai loop pemrosesan tak terbatas. Handler mengirim pesan dan Runnable ke antrian ini. Perbedaan utama: RunLoop mendukung mode (modes), sedangkan Android Looper tidak. Looper memproses semua pesan tanpa memfilter berdasarkan mode, yang membuatnya lebih sederhana tetapi kurang fleksibel dalam skenario dengan prioritas (misalnya, scroll di iOS diproses dalam mode .tracking terpisah dari acara lain).

Mode RunLoop: default, tracking, common

RunLoop Mode — adalah kumpulan sumber, timer, dan pengamat yang aktif saat ini. Mode memungkinkan isolasi pemrosesan acara berdasarkan prioritas. Ketika pengguna menggulir UITableView, RunLoop beralih ke mode .tracking, di mana hanya acara scroll dan timer/animasi terkait yang diproses. Semua sumber lain (misalnya, NSURLConnection) ditangguhkan sampai keluar dari mode scroll.

Tiga mode utama: .default (NSDefaultRunLoopMode) — mode utama di mana semua acara kecuali scroll diproses; .tracking (UITrackingRunLoopMode) — diaktifkan saat scroll atau navigasi gesture; .common (NSRunLoopCommonModes) — bukan mode terpisah, melainkan kumpulan alias yang mencakup .default + .tracking. Menambahkan sumber ke .commonModes secara otomatis menambahkannya ke semua mode dalam kumpulan.

ModeKonstanta Core FoundationKonstanta FoundationKapan aktif
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultKeadaan normal, tanpa scroll
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingScroll, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonMode pseudo: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModePertama kali RunLoop dijalankan

Mengapa NSTimer Tidak Bekerja Saat Scroll

Masalah klasik: NSTimer yang ditambahkan dalam mode .default berhenti bekerja saat scroll, karena RunLoop beralih ke mode .tracking dan tidak memproses timer dari .default. Solusi — tambahkan timer ke .commonModes: RunLoop.current.add(timer, forMode: .common). Ini akan membuat timer bekerja baik di .default maupun .tracking. Alternatifnya adalah menggunakan DispatchQueue.main.async alih-alih NSTimer, karena GCD bekerja di tingkat thread, bukan mode RunLoop.

RunLoop di Thread Latar Belakang

Thread latar belakang tidak memiliki RunLoop secara default. Jika di thread latar belakang perlu menjalankan NSTimer, memproses NSInputStream/NSOutputStream, atau bereaksi terhadap performSelector:, Anda harus membuat dan menjalankan RunLoop secara manual. Tanpa RunLoop, timer dan performSelector: tidak akan pernah bekerja — thread akan menjalankan kode dan selesai tanpa menunggu acara.

Untuk membuat RunLoop di thread latar belakang, cukup panggil RunLoop.current.run() di akhir kerja thread. Panggilan ini memblokir thread untuk waktu tidak terbatas, memproses acara. Untuk berhenti, gunakan CFRunLoopStop(CFRunLoopGetCurrent()). Penting: RunLoop.current membuat RunLoop secara malas pada akses pertama — jika Anda tidak memanggil run(), ia tidak akan memproses acara. Pola: konfigurasi sumber -> tambahkan ke RunLoop -> panggil run().

swift
import Foundation

// Thread latar belakang dengan RunLoop sendiri
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // RunLoop dibuat otomatis saat RunLoop.current dipanggil
            let runLoop = RunLoop.current

            // Kami menambahkan port untuk menjaga RunLoop tetap aktif
            runLoop.add(Port(), forMode: .default)

            // Memulai pemrosesan acara
            var isFinished = false
            while !isFinished {
                // run(mode:before:) mengembalikan true jika acara diproses
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

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

    func stop() {
        // Menghentikan RunLoop di thread latar belakang
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

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

// Penggunaan: timer di RunLoop latar belakang
let manager = BackgroundRunLoopManager()
manager.start()

// Mengirim tugas ke RunLoop latar belakang melalui performSelector
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager membuat thread latar belakang dengan RunLoop permanen. Menambahkan Port() kosong diperlukan untuk mencegah RunLoop berakhir segera — tanpa sumber, RunLoop.run() mengembalikan false dan keluar. performSelector:onThread: mengirim pesan ke RunLoop latar belakang — akan diproses ketika RunLoop memasuki fase BeforeSources. Stop memanggil CFRunLoopStop pada thread latar belakang, mengakhiri siklus.

NSTimer membuat acara timer yang diproses RunLoop dalam fase BeforeTimers. Timer bisa repeating (berulang) dan non-repeating (sekali). NSTimer tidak menjamin ketepatan: jika RunLoop diblokir oleh operasi panjang, timer akan aktif setelah pemblokiran, dan semua aktivasi yang terlewat akan digabung menjadi satu (untuk timer berulang — paling banyak satu aktivasi “pengejaran”).

CADisplayLink — timer khusus yang disinkronkan dengan frekuensi penyegaran layar (60/120/144 Hz). Digunakan untuk animasi dan pembaruan video. CADisplayLink ditambahkan ke RunLoop dan aktif sebelum setiap frame rendering (sebelum Core Animation mengirim lapisan ke rendering). Jika sebuah frame terlewat (display link tidak bisa aktif dalam 16 ms), panggilan berikutnya terjadi di siklus VSync berikutnya.

swift
import UIKit

class AnimationController {

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

    // CADisplayLink — animasi dengan vsync
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // Menambahkan dalam mode .common — bekerja juga saat scroll
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // Dipanggil setiap frame (60 FPS → setiap 16.6 ms)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // berhenti setelah 5 detik
        }
    }

    // NSTimer — tugas periodik
    func startTimerInCommonMode() {
        displayLinkTimer?.invalidate()
        displayLinkTimer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: true
        ) { [weak self] timer in
            print("Timer tick")
        }

        // KUNCI: tambahkan di .common, jika tidak timer membeku saat scroll
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

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

Di AnimationController, CADisplayLink ditambahkan dalam mode .common, yang menjamin pemanggilan step pada setiap frame terlepas dari scroll. displayLink.add(to: .current, forMode: .common) — pola standar untuk animasi yang tidak boleh terputus saat scroll. NSTimer juga ditambahkan dalam mode .common agar tetap berdetak saat scroll. Tanpa ini, timer hanya akan bekerja dalam mode .default.

RunLoop Observers: pemantauan siklus acara

CFRunLoopObserver — mekanisme untuk melacak fase RunLoop. Dengan Observer, Anda dapat menerima pemberitahuan tentang masuk ke mode, mulai pemrosesan timer, mulai pemrosesan sumber, terbangun dari tidur, keluar dari mode. Observer digunakan oleh framework untuk kebutuhan mereka: Core Animation menggunakannya untuk rendering lapisan sebelum RunLoop tidur, UIKit — untuk memperbarui layout setelah pemrosesan acara.

Pengembang juga dapat menambahkan Observer untuk tujuan sendiri. Misalnya: mengukur waktu pemrosesan acara (profil), menjalankan operasi yang ditunda sebelum RunLoop tidur (ketika UI sudah diperbarui dan pengguna tidak berinteraksi), menyimpan data otomatis saat tidak aktivitas lama. Observer didaftarkan melalui CFRunLoopAddObserver dengan menentukan mode dan bit mask dari aktivitas yang dilacak.

Titik paling berguna untuk Observer: .afterWaiting — dijalankan setelah RunLoop terbangun dan dapat berisi kode yang harus dijalankan setelah pemrosesan acara; .beforeTimers — sebelum pemrosesan timer, memungkinkan pengukuran waktu sejak pemrosesan sebelumnya; .exit — aktif saat RunLoop berhenti, berguna untuk membersihkan sumber daya thread latar belakang.

CFRunLoopStop dan akhir siklus

CFRunLoopStop — fungsi yang memaksa mengakhiri iterasi RunLoop saat ini. Saat CFRunLoopStop(CFRunLoopGetCurrent()) dipanggil, RunLoop mengakhiri pemrosesan acara saat ini dan keluar dari run(), mengembalikan false. Ini adalah cara standar untuk menghentikan RunLoop di thread latar belakang. Di Main Thread, CFRunLoopStop tidak disarankan — RunLoop utama harus bekerja sepanjang umur aplikasi. Untuk thread latar belakang, setelah CFRunLoopStop thread dapat berakhir atau melanjutkan eksekusi kode berikutnya setelah run().

Pertanyaan Umum

Apa itu RunLoop di iOS?

RunLoop — siklus acara di iOS yang diimplementasikan oleh CFRunLoop (Core Foundation) dan NSRunLoop (Foundation). Menunggu acara (sentuhan, timer, sumber input) dan mengirimkannya ke handler di thread. Setiap thread dapat memiliki satu RunLoop, tetapi secara otomatis hanya dibuat untuk Main Thread. RunLoop mengelola mode (.default, .tracking, .common), mengisolasi pemrosesan berdasarkan prioritas.

Mengapa NSTimer tidak bekerja saat scroll?

NSTimer secara default ditambahkan dalam mode .default RunLoop. Saat pengguna scroll, RunLoop beralih ke mode .tracking dan tidak memproses timer dari .default. Solusi: tambahkan timer dalam mode .common melalui RunLoop.current.add(timer, forMode: .common). .common menggabungkan .default dan .tracking, sehingga timer bekerja di kedua mode.

Apakah perlu menjalankan RunLoop di thread latar belakang?

Hanya jika thread latar belakang menggunakan timer (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream, atau acara Source. Jika thread menjalankan tugas sinkron (mengunduh file, perhitungan) dan selesai — RunLoop tidak diperlukan. Untuk menjalankan, panggil RunLoop.current.run() setelah konfigurasi sumber. Untuk berhenti — CFRunLoopStop(CFRunLoopGetCurrent()).

Apa perbedaan RunLoop dengan GCD DispatchQueue?

RunLoop bekerja di tingkat thread dan memproses acara secara berurutan, dengan dukungan mode (modes). DispatchQueue — abstraksi kumpulan thread, tugas dijalankan pada thread kosong mana pun. GCD tidak mendukung mode dan hidup independen dari RunLoop. DispatchQueue.main menggunakan RunLoop utama untuk menjalankan blok — ini satu-satunya titik persimpangan. Untuk tugas latar belakang, GCD lebih disukai.

Bagaimana CADisplayLink terkait dengan RunLoop?

CADisplayLink — timer yang disinkronkan dengan VSync (frekuensi penyegaran layar). Ditambahkan ke RunLoop dan aktif sebelum setiap frame rendering dalam fase BeforeTimers. CADisplayLink hanya bekerja di Main Thread, karena rendering layar terjadi di sana. Untuk animasi berkelanjutan saat scroll, tambahkan dalam mode .common: displayLink.add(to: .current, forMode: .common).

Kesimpulan

  • RunLoop — event loop iOS: memproses sentuhan, timer, sumber input di thread; Main Thread memiliki RunLoop otomatis
  • Tiga mode: .default (umum), .tracking (scroll), .common (default + tracking) — mengelola penyaringan acara
  • NSTimer di .default tidak bekerja saat scroll — solusi: tambahkan ke mode .common melalui RunLoop.current.add(timer, forMode: .common)
  • Thread latar belakang tidak memiliki RunLoop default — untuk timer dan performSelector: perlu menjalankan manual melalui RunLoop.current.run()
  • CADisplayLink — timer untuk setiap frame VSync, penting untuk animasi halus; ditambahkan ke .common untuk bekerja saat scroll
  • RunLoop Observer — pemantauan fase: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; digunakan untuk profiling
  • RunLoop ≠ Looper: iOS RunLoop mendukung mode dan timer, Android Looper lebih sederhana — tanpa mode, Handler + MessageQueue

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga