RunLoop در iOS: چیست، حالت‌های کار و چرخه رویداد

نویسنده: IT Sectr منتشر شده: 2026-03-16 زمان مطالعه: 11 دقیقه

RunLoop — چرخه پردازش رویداد در iOS که توسط اشیاء CFRunLoop (Core Foundation) و NSRunLoop (Foundation) پیاده‌سازی شده است. این مکانیزمی است که منتظر رویدادها (لمس‌ها، تایمرها، منابع ورودی، اعلان‌ها) می‌ماند و آنها را به handlerهای مربوطه در رشته ارسال می‌کند. هر رشته در iOS حداکثر یک RunLoop دارد، اما به طور خودکار فقط برای Main Thread ایجاد می‌شود. به گفته مستندات Apple CFRunLoop، RunLoop برای کار تایمرها، انیمیشن‌ها و نظارت بر منابع در رشته‌های پس‌زمینه حیاتی است.

مهمترین نکات

  • RunLoop — event loop که رویدادها را در رشته پردازش می‌کند: لمس‌ها، تایمرها، منابع ورودی
  • Main Thread دارای RunLoop خودکار است (CFRunLoopGetMain())، رشته‌های پس‌زمینه باید دستی راه‌اندازی شوند
  • سه حالت: .default (اصلی)، .tracking (اسکرول)، .common (ترکیب default+tracking)
  • NSTimer و CADisplayLink بدون RunLoop فعال روی رشته کار نمی‌کنند
  • RunLoop observers امکان واکنش به ورود/خروج از حالت‌ها و شروع/پایان پردازش را فراهم می‌کنند

RunLoop چیست

RunLoop — یک شی زیرساختی Core Foundation است که پردازش رویدادها را روی رشته سازماندهی می‌کند. در اصل این یک حلقه بی‌نهایت (while true) است که منتظر رسیدن رویدادها (sources) می‌ماند و آنها را به handlerها منتقل می‌کند. وقتی رویدادی وجود ندارد، RunLoop رشته را به حالت خواب (sleep) می‌برد و در مصرف باتری صرفه‌جویی می‌کند. با رسیدن رویداد، رشته بیدار می‌شود، آن را پردازش می‌کند و دوباره به خواب می‌رود. RunLoop فقط در iOS/macOS (XNU + Core Foundation) وجود دارد — در Android نقش آن را Looper ایفا می‌کند.

هر رشته حداکثر یک RunLoop دارد که به صورت تنبل (lazy) در اولین دسترسی ایجاد می‌شود. برای Main Thread، RunLoop به طور خودکار هنگام راه‌اندازی برنامه ایجاد می‌شود. برای رشته‌های پس‌زمینه، RunLoop تا زمانی که CFRunLoopGetCurrent() یا RunLoop.current فراخوانی نشود، ایجاد نمی‌شود. RunLoop اصلی برنامه مسئول پردازش رویدادهای لمسی، رندرینگ صفحه، اجرای بلوک‌های DispatchQueue.main و سرویس‌دهی به لایه‌های Core Animation است.

RunLoop یک رشته نیست — این مکانیزمی درون رشته است. رشته می‌تواند بدون RunLoop وجود داشته باشد (اگر کار همزمان انجام دهد و تمام شود)، اما RunLoop نمی‌تواند بدون رشته وجود داشته باشد. وقتی رشته با RunLoop فعال رویدادی ندارد، CPU را مسدود نمی‌کند، بلکه در حالت انتظار (waiting) قرار می‌گیرد — این تفاوت کلیدی با busy-wait است که 100% CPU مصرف می‌کند.

RunLoop چگونه کار می‌کند: آناتومی چرخه رویداد

RunLoop دو نوع منبع رویداد را پردازش می‌کند: Input Sources (منابع ورودی) و Timer Sources (تایمرها). Input Sources رویدادهای ناهمگام را تحویل می‌دهند: لمس‌ها، حرکات ماوس، داده‌های سوکت، پیام‌های رشته‌های دیگر (performSelector:onThread:). Timer Sources رویدادهای همگام را طبق برنامه تحویل می‌دهند: NSTimer، CADisplayLink. همچنین Observers — نقاط ورود برای نظارت بر وضعیت RunLoop — وجود دارند.

چرخه RunLoop از مراحل متوالی تشکیل شده است: ورود به حالت (kCFRunLoopEntry)، پردازش تایمرها (kCFRunLoopBeforeTimers)، پردازش منابع ورودی (kCFRunLoopBeforeSources)، پردازش منابع (kCFRunLoopAfterWaiting)، انتظار (sleep)، خروج از حالت (kCFRunLoopExit). اگر در تکرار فعلی هیچ رویدادی پردازش نشود، RunLoop رشته را برای مدت نامحدود به خواب می‌فرستد تا توسط یک رویداد جدید بیدار شود.

swift
import Foundation

// نمایش فازهای RunLoop از طریق 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 فعال شد")
        case .beforeTimers:
            print("BeforeTimers — پردازش تایمرها")
        case .beforeSources:
            print("BeforeSources — پردازش منابع")
        case .afterWaiting:
            print("AfterWaiting — بیدار شدن پس از sleep")
        case .exit:
            print("Exit — RunLoop پایان یافت")
        default:
            break
        }
    }

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

// مثال: RunLoop تایمر را روی رشته اصلی پردازش می‌کند
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // RunLoop.current.run() روی Main Thread توسط UIApplicationMain فراخوانی می‌شود
    // به طور خودکار — نیازی به راه‌اندازی دستی نیست
    RunLoop.current.run() // این فراخوانی به Main Thread برنمی‌گردد
}

مثال observeRunLoopActivities یک Observer را روی RunLoop اصلی ثبت می‌کند که هر مرحله از چرخه را لاگ می‌کند. این برای اشکال‌زدایی مفید است: اگر فاصله طولانی بین .beforeTimers و .afterWaiting می‌بینید، یعنی RunLoop توسط یک عملیات روی Main Thread مسدود شده است. timerOnMainRunLoop نشان می‌دهد که چگونه NSTimer به طور خودکار روی RunLoop اصلی کار می‌کند — هنگام ایجاد Timer.scheduledTimer، تایمر به RunLoop جاری به طور پیش‌فرض (.default mode) اضافه می‌شود.

RunLoop در مقابل Looper (Android)

Android Looper — معادل RunLoop است. Looper.prepare() یک صف پیام (MessageQueue) روی رشته ایجاد می‌کند، Looper.loop() یک حلقه بی‌نهایت پردازش را راه‌اندازی می‌کند. Handler پیام‌ها و Runnableها را به این صف ارسال می‌کند. تفاوت اصلی: RunLoop از حالت‌ها (modes) پشتیبانی می‌کند، اما Android Looper پشتیبانی نمی‌کند. Looper همه پیام‌ها را بدون فیلتر کردن بر اساس حالت پردازش می‌کند که آن را ساده‌تر اما در سناریوهای دارای اولویت انعطاف‌پذیرتر می‌کند (مثلاً اسکرول در iOS در حالت .tracking جدا از سایر رویدادها پردازش می‌شود).

حالت‌های RunLoop: default, tracking, common

RunLoop Mode — مجموعه‌ای از منابع، تایمرها و ناظران است که در حال حاضر فعال هستند. حالت‌ها امکان جداسازی پردازش رویدادها را بر اساس اولویت فراهم می‌کنند. وقتی کاربر UITableView را اسکرول می‌کند، RunLoop به حالت .tracking سوئیچ می‌کند که در آن فقط رویدادهای اسکرول و تایمرها/انیمیشن‌های مربوطه پردازش می‌شوند. سایر منابع (مثلاً NSURLConnection) تا خروج از حالت اسکرول متوقف می‌شوند.

سه حالت اصلی: .default (NSDefaultRunLoopMode) — حالت اصلی که در آن همه رویدادها به جز اسکرول پردازش می‌شوند؛ .tracking (UITrackingRunLoopMode) — هنگام اسکرول یا ناوبری حرکتی فعال می‌شود؛ .common (NSRunLoopCommonModes) — یک حالت مجزا نیست، بلکه مجموعه‌ای از نام‌های مستعار است که .default + .tracking را شامل می‌شود. افزودن منبع به .commonModes به طور خودکار آن را به همه حالت‌های مجموعه اضافه می‌کند.

حالتثابت Core Foundationثابت Foundationزمان فعال بودن
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultوضعیت عادی، بدون اسکرول
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingاسکرول، gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonحالت شبه: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeاولین راه‌اندازی RunLoop

چرا NSTimer در هنگام اسکرول کار نمی‌کند

مشکل کلاسیک: NSTimer اضافه شده به حالت .default در هنگام اسکرول از کار می‌افتد، زیرا RunLoop به حالت .tracking سوئیچ می‌کند و تایمرهای .default را پردازش نمی‌کند. راه‌حل — تایمر را به .commonModes اضافه کنید: RunLoop.current.add(timer, forMode: .common). این باعث می‌شود تایمر هم در .default و هم در .tracking کار کند. جایگزین استفاده از DispatchQueue.main.async به جای NSTimer است، زیرا GCD در سطح رشته‌ها کار می‌کند، نه حالت‌های RunLoop.

RunLoop در رشته‌های پس‌زمینه

رشته‌های پس‌زمینه به طور پیش‌فرض RunLoop ندارند. اگر در رشته پس‌زمینه نیاز به راه‌اندازی NSTimer، پردازش NSInputStream/NSOutputStream یا واکنش به performSelector: دارید، باید RunLoop را دستی ایجاد و راه‌اندازی کنید. بدون RunLoop، تایمر و performSelector: هرگز کار نخواهند کرد — رشته کد را اجرا کرده و بدون انتظار برای رویدادها پایان می‌یابد.

برای ایجاد RunLoop در رشته پس‌زمینه، کافی است در پایان کار رشته RunLoop.current.run() را فراخوانی کنید. این فراخوانی رشته را به طور نامحدود مسدود می‌کند و رویدادها را پردازش می‌کند. برای توقف از CFRunLoopStop(CFRunLoopGetCurrent()) استفاده کنید. مهم: RunLoop.current به صورت تنبل در اولین دسترسی RunLoop ایجاد می‌کند — اگر run() را فراخوانی نکنید، رویدادها را پردازش نخواهد کرد. الگو: پیکربندی منابع -> اضافه به RunLoop -> فراخوانی run().

swift
import Foundation

// رشته پس‌زمینه با RunLoop اختصاصی
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // RunLoop به طور خودکار هنگام فراخوانی RunLoop.current ایجاد می‌شود
            let runLoop = RunLoop.current

            // برای فعال نگه داشتن RunLoop یک پورت اضافه می‌کنیم
            runLoop.add(Port(), forMode: .default)

            // پردازش رویدادها را راه‌اندازی می‌کنیم
            var isFinished = false
            while !isFinished {
                // run(mode:before:) در صورت پردازش رویداد true برمی‌گرداند
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

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

    func stop() {
        // توقف RunLoop در رشته پس‌زمینه
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

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

// استفاده: تایمر روی RunLoop پس‌زمینه
let manager = BackgroundRunLoopManager()
manager.start()

// ارسال وظیفه به RunLoop پس‌زمینه از طریق performSelector
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager یک رشته پس‌زمینه با RunLoop دائمی ایجاد می‌کند. افزودن Port() خالی برای جلوگیری از پایان فوری RunLoop ضروری است — بدون منابع، RunLoop.run() false برمی‌گرداند و خارج می‌شود. performSelector:onThread: پیامی به RunLoop پس‌زمینه ارسال می‌کند — زمانی که RunLoop وارد فاز BeforeSources شود پردازش خواهد شد. Stop با فراخوانی CFRunLoopStop روی رشته پس‌زمینه چرخه را پایان می‌دهد.

NSTimer یک رویداد تایمر ایجاد می‌کند که RunLoop آن را در فاز BeforeTimers پردازش می‌کند. تایمرها می‌توانند repeating (تکرارشونده) و non-repeating (یک‌باره) باشند. NSTimer دقت را تضمین نمی‌کند: اگر RunLoop توسط یک عملیات طولانی مسدود شده باشد، تایمر پس از رفع انسداد فعال می‌شود و همه فعال‌سازی‌های از دست رفته در یکی ادغام می‌شوند (برای تایمر تکراری — حداکثر یک فعال‌سازی «جبرانی»).

CADisplayLink — یک تایمر تخصصی همگام‌سازی شده با نرخ تازه‌سازی صفحه (60/120/144 هرتز). برای انیمیشن‌ها و به‌روزرسانی ویدئو استفاده می‌شود. CADisplayLink به RunLoop اضافه می‌شود و قبل از هر فریم رندرینگ (قبل از اینکه Core Animation لایه را به رندر بفرستد) فعال می‌شود. اگر فریمی از دست رفته باشد (display link نتواند در 16 میلی‌ثانیه فعال شود)، فراخوانی بعدی در چرخه VSync بعدی رخ می‌دهد.

swift
import UIKit

class AnimationController {

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

    // CADisplayLink — انیمیشن با vsync
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // افزودن به حالت .common — حتی در هنگام اسکرول کار می‌کند
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // در هر فریم فراخوانی می‌شود (60 FPS → هر 16.6 میلی‌ثانیه)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // توقف پس از ۵ ثانیه
        }
    }

    // NSTimer — وظیفه دوره‌ای
    func startTimerInCommonMode() {
        displayLinkTimer?.invalidate()
        displayLinkTimer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: true
        ) { [weak self] timer in
            print("Timer tick")
        }

        // نکته کلیدی: به .common اضافه می‌کنیم، وگرنه تایمر در اسکرول متوقف می‌شود
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

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

در AnimationController، CADisplayLink به حالت .common اضافه شده است که فراخوانی step را در هر فریم بدون توجه به اسکرول تضمین می‌کند. displayLink.add(to: .current, forMode: .common) — الگوی استاندارد برای انیمیشن‌هایی که نباید در هنگام اسکرول قطع شوند. NSTimer نیز به حالت .common اضافه شده است تا در هنگام اسکرول تیک بزند. بدون این، تایمر فقط در حالت .default کار می‌کرد.

RunLoop Observers: نظارت بر رویدادهای چرخه

CFRunLoopObserver — مکانیزمی برای ردیابی فازهای RunLoop. با استفاده از Observer می‌توان اعلان‌هایی درباره ورود به حالت، شروع پردازش تایمرها، شروع پردازش منابع، بیدار شدن از خواب، خروج از حالت دریافت کرد. Observerها توسط فریم‌ورک‌ها برای نیازهای خود استفاده می‌شوند: Core Animation از آنها برای رندر کردن لایه‌ها قبل از خواب RunLoop استفاده می‌کند، UIKit — برای به‌روزرسانی layout پس از پردازش رویدادها.

توسعه‌دهنده نیز می‌تواند Observerها را برای اهداف خود اضافه کند. به عنوان مثال: اندازه‌گیری زمان پردازش رویدادها (پروفایلینگ)، اجرای عملیات معوق قبل از خواب RunLoop (زمانی که UI به‌روز شده و کاربر تعاملی ندارد)، ذخیره خودکار داده‌ها در زمان بی‌عملی طولانی. Observer از طریق CFRunLoopAddObserver با تعیین حالت و ماسک بیتی فعالیت‌های ردیابی‌شده ثبت می‌شود.

مفیدترین نقاط برای Observer: .afterWaiting — پس از بیدار شدن RunLoop اجرا می‌شود و می‌تواند کدی را که باید پس از پردازش رویداد اجرا شود شامل شود؛ .beforeTimers — قبل از پردازش تایمرها، امکان اندازه‌گیری زمان سپری‌شده از پردازش قبلی را فراهم می‌کند؛ .exit — هنگام توقف RunLoop فعال می‌شود، برای پاک‌سازی منابع رشته پس‌زمینه مفید است.

CFRunLoopStop و پایان چرخه

CFRunLoopStop — تابعی که به اجبار تکرار فعلی RunLoop را پایان می‌دهد. هنگام فراخوانی CFRunLoopStop(CFRunLoopGetCurrent())، RunLoop پردازش رویداد جاری را پایان می‌دهد و از run() با بازگرداندن false خارج می‌شود. این روش استاندارد برای توقف RunLoop در رشته پس‌زمینه است. در Main Thread، CFRunLoopStop توصیه نمی‌شود — RunLoop اصلی باید در تمام طول عمر برنامه کار کند. برای رشته‌های پس‌زمینه، پس از CFRunLoopStop رشته می‌تواند پایان یابد یا به اجرای کد بعدی پس از run() ادامه دهد.

سوالات متداول

RunLoop در iOS چیست؟

RunLoop — چرخه رویداد در iOS که توسط CFRunLoop (Core Foundation) و NSRunLoop (Foundation) پیاده‌سازی شده است. منتظر رویدادها (لمس‌ها، تایمرها، منابع ورودی) می‌ماند و آنها را به handlerها در رشته ارسال می‌کند. هر رشته می‌تواند یک RunLoop داشته باشد، اما به طور خودکار فقط برای Main Thread ایجاد می‌شود. RunLoop حالت‌ها (.default، .tracking، .common) را مدیریت می‌کند و پردازش را بر اساس اولویت جدا می‌کند.

چرا NSTimer در هنگام اسکرول کار نمی‌کند؟

NSTimer به طور پیش‌فرض به حالت .default RunLoop اضافه می‌شود. وقتی کاربر اسکرول می‌کند، RunLoop به حالت .tracking سوئیچ می‌کند و تایمرهای .default را پردازش نمی‌کند. راه‌حل: تایمر را به حالت .common اضافه کنید: RunLoop.current.add(timer, forMode: .common). .common .default و .tracking را ترکیب می‌کند، بنابراین تایمر در هر دو حالت کار می‌کند.

آیا راه‌اندازی RunLoop در رشته پس‌زمینه لازم است؟

فقط اگر رشته پس‌زمینه از تایمرها (NSTimer)، performSelector:onThread:، NSInputStream/NSOutputStream یا رویدادهای Source استفاده کند. اگر رشته یک کار همگام (دانلود فایل، محاسبات) انجام می‌دهد و پایان می‌یابد — RunLoop لازم نیست. برای راه‌اندازی، پس از پیکربندی منابع RunLoop.current.run() را فراخوانی کنید. برای توقف — CFRunLoopStop(CFRunLoopGetCurrent()).

چه تفاوتی بین RunLoop و GCD DispatchQueue وجود دارد؟

RunLoop در سطح رشته کار می‌کند و رویدادها را به صورت ترتیبی با پشتیبانی از حالت‌ها (modes) پردازش می‌کند. DispatchQueue — انتزاعی از استخر رشته‌ها است، وظایف در هر رشته آزاد اجرا می‌شوند. GCD از حالت‌ها پشتیبانی نمی‌کند و مستقل از RunLoop وجود دارد. DispatchQueue.main از RunLoop اصلی برای اجرای بلوک‌ها استفاده می‌کند — این تنها نقطه تقاطع است. برای وظایف پس‌زمینه، GCD ترجیح داده می‌شود.

CADisplayLink چگونه با RunLoop مرتبط است؟

CADisplayLink — تایمری همگام‌سازی شده با VSync (نرخ تازه‌سازی صفحه). به RunLoop اضافه می‌شود و قبل از هر فریم رندرینگ در فاز BeforeTimers فعال می‌شود. CADisplayLink فقط روی Main Thread کار می‌کند، زیرا رندرینگ صفحه در آنجا انجام می‌شود. برای انیمیشن‌های پیوسته در هنگام اسکرول، آن را به حالت .common اضافه کنید: displayLink.add(to: .current, forMode: .common).

نتیجه‌گیری

  • RunLoop — چرخه رویداد iOS: لمس‌ها، تایمرها، منابع ورودی را در رشته پردازش می‌کند؛ Main Thread دارای RunLoop خودکار است
  • سه حالت: .default (عمومی)، .tracking (اسکرول)، .common (default + tracking) — فیلتر کردن رویدادها را مدیریت می‌کنند
  • NSTimer در .default در هنگام اسکرول کار نمی‌کند — راه‌حل: اضافه کردن به حالت .common از طریق RunLoop.current.add(timer, forMode: .common)
  • رشته‌های پس‌زمینه به طور پیش‌فرض RunLoop ندارند — برای تایمرها و performSelector: راه‌اندازی دستی از طریق RunLoop.current.run() لازم است
  • CADisplayLink — تایمر برای هر فریم VSync، برای انیمیشن‌های روان ضروری است؛ برای کار در هنگام اسکرول به .common اضافه می‌شود
  • RunLoop Observer — نظارت بر فازها: Entry، BeforeTimers، BeforeSources، AfterWaiting، Exit؛ برای پروفایلینگ استفاده می‌شود
  • RunLoop ≠ Looper: iOS RunLoop از حالت‌ها و تایمرها پشتیبانی می‌کند، Android Looper ساده‌تر است — بدون حالت، Handler + MessageQueue

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید