RunLoop i iOS: vad är det, arbetslägen och händelsecykel

Författare: IT Sectr Publicerad: 2026-03-16 Lästid: 11 min

RunLoop — händelseloopen i iOS, implementerad av objekten CFRunLoop (Core Foundation) och NSRunLoop (Foundation). Detta är en mekanism som väntar på händelser (beröringar, timers, inmatningskällor, meddelanden) och skickar dem till motsvarande hanterare på tråden. Varje tråd i iOS har högst en RunLoop, men den skapas automatiskt endast för Main Thread. Enligt Apples CFRunLoop-dokumentation är RunLoop avgörande för timers, animeringar och övervakning av källor i bakgrundstrådar.

Viktigast

  • RunLoop — event loop som bearbetar händelser på tråden: beröringar, timers, inmatningskällor
  • Main Thread har automatisk RunLoop (CFRunLoopGetMain()), bakgrundstrådar måste startas manuellt
  • Tre lägen: .default (huvud), .tracking (scrollning), .common (kombinerar default+tracking)
  • NSTimer och CADisplayLink fungerar inte utan aktiv RunLoop på tråden
  • RunLoop observers gör det möjligt att reagera på in-/utgång ur lägen och början/slut på bearbetning

Vad är RunLoop

RunLoop — är ett infrastrukturobjekt i Core Foundation som organiserar bearbetning av händelser på tråden. I princip är det en oändlig loop (while true) som väntar på att händelser (sources) ska komma och skickar dem vidare till hanterare. När det inte finns några händelser lägger RunLoop tråden i sömn (sleep) och sparar batterienergi. När en händelse anländer vaknar tråden, bearbetar den och somnar om. RunLoop finns bara i iOS/macOS (XNU + Core Foundation) — i Android spelas dess roll av Looper.

Varje tråd har högst en RunLoop, som skapas lat (lazy) vid första åtkomst. För Main Thread skapas RunLoop automatiskt när appen startar. För bakgrundstrådar skapas inte RunLoop förrän CFRunLoopGetCurrent() eller RunLoop.current anropas. Appens huvud-RunLoop ansvarar för att bearbeta beröringshändelser, skärmrendering, exekvering av DispatchQueue.main-block och betjäning av Core Animation-lager.

RunLoop är inte en tråd — det är en mekanism inuti tråden. En tråd kan existera utan RunLoop (om den utför en synkron uppgift och avslutas), men RunLoop kan inte existera utan en tråd. När en tråd med aktiv RunLoop inte har några händelser, blockerar den inte CPU, utan är i vänteläge (waiting) — detta är den viktigaste skillnaden från busy-wait som förbrukar 100% CPU.

Hur RunLoop fungerar: anatomi av händelseloopen

RunLoop bearbetar två typer av händelsekällor: Input Sources (inmatningskällor) och Timer Sources (timers). Input Sources levererar asynkrona händelser: beröringar, musrörelser, data från socket, meddelanden från andra trådar (performSelector:onThread:). Timer Sources levererar synkrona händelser enligt schema: NSTimer, CADisplayLink. Det finns också Observers — ingångspunkter för att övervaka RunLoop-status.

RunLoop-cykeln består av sekventiella faser: inträde i läge (kCFRunLoopEntry), bearbetning av timers (kCFRunLoopBeforeTimers), bearbetning av inmatningskällor (kCFRunLoopBeforeSources), bearbetning av källor (kCFRunLoopAfterWaiting), väntan (sleep), utgång ur läge (kCFRunLoopExit). Om ingen händelse har bearbetats i den aktuella iterationen, lägger RunLoop tråden i sömn på obestämd tid tills den väcks av en ny händelse.

swift
import Foundation

// Demonstration av RunLoop-faser genom 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 aktiverad")
        case .beforeTimers:
            print("BeforeTimers — bearbetning av timers")
        case .beforeSources:
            print("BeforeSources — bearbetning av källor")
        case .afterWaiting:
            print("AfterWaiting — uppvaknande efter sömn")
        case .exit:
            print("Exit — RunLoop avslutad")
        default:
            break
        }
    }

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

// Exempel: RunLoop bearbetar en timer på huvudtråden
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // RunLoop.current.run() på Main Thread anropas av UIApplicationMain
    // automatiskt — behöver inte startas manuellt
    RunLoop.current.run() // Detta anrop återvänder inte till Main Thread
}

Exemplet observeRunLoopActivities registrerar en Observer på huvud-RunLoop som loggar varje fas av cykeln. Detta är användbart för felsökning: om du ser ett långt intervall mellan .beforeTimers och .afterWaiting betyder det att RunLoop är blockerad av en operation på Main Thread. timerOnMainRunLoop visar hur NSTimer automatiskt fungerar på huvud-RunLoop — när Timer.scheduledTimer skapas läggs timern till i den aktuella RunLoop som standard (.default mode).

RunLoop vs Looper (Android)

Android Looper — motsvarigheten till RunLoop. Looper.prepare() skapar en meddelandekö (MessageQueue) på tråden, Looper.loop() startar en oändlig bearbetningsloop. Handler skickar meddelanden och Runnable till denna kö. Huvudskillnaden: RunLoop stöder lägen (modes), medan Android Looper inte gör det. Looper bearbetar alla meddelanden utan filtrering efter läge, vilket gör det enklare men mindre flexibelt i scenarier med prioriteringar (t.ex. scrollning i iOS bearbetas i .tracking-läge separat från andra händelser).

RunLoop-lägen: default, tracking, common

RunLoop Mode — är en uppsättning källor, timers och observatörer som för närvarande är aktiva. Lägen gör det möjligt att isolera händelsebearbetning efter prioritet. När användaren scrollar en UITableView växlar RunLoop till .tracking-läge, där endast scrollhändelser och motsvarande timers/animeringar bearbetas. Alla andra källor (t.ex. NSURLConnection) pausas tills scrolläget lämnas.

Tre huvudsakliga lägen: .default (NSDefaultRunLoopMode) — huvudläget där alla händelser utom scrollning bearbetas; .tracking (UITrackingRunLoopMode) — aktiveras vid scrollning eller gestnavigering; .common (NSRunLoopCommonModes) — är inte ett separat läge, utan en uppsättning alias som inkluderar .default + .tracking. Att lägga till en källa i .commonModes lägger automatiskt till den i alla lägen i uppsättningen.

LägeCore Foundation-konstantFoundation-konstantNär aktivt
.defaultkCFRunLoopDefaultModeRunLoop.Mode.defaultNormalt tillstånd, utan scrollning
.trackingUITrackingRunLoopModeRunLoop.Mode.trackingScrollning, gesture recognizers
.commonkCFRunLoopCommonModesRunLoop.Mode.commonPseudo-läge: default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeFörsta starten av RunLoop

Varför NSTimer inte fungerar under scrollning

Klassiskt problem: NSTimer som läggs till i .default-läge slutar fungera under scrollning, eftersom RunLoop växlar till .tracking-läge och inte bearbetar timers från .default. Lösning — lägg till timern i .commonModes: RunLoop.current.add(timer, forMode: .common). Detta gör att timern fungerar både i .default och .tracking. Ett alternativ är att använda DispatchQueue.main.async istället för NSTimer, eftersom GCD fungerar på trådnivå, inte på RunLoop-lägesnivå.

RunLoop i bakgrundstrådar

Bakgrundstrådar har ingen RunLoop som standard. Om du i en bakgrundstråd behöver starta NSTimer, bearbeta NSInputStream/NSOutputStream eller reagera på performSelector:, måste du manuellt skapa och starta RunLoop. Utan RunLoop kommer timern och performSelector: aldrig att fungera — tråden kör koden och avslutas utan att vänta på händelser.

För att skapa en RunLoop i en bakgrundstråd räcker det att anropa RunLoop.current.run() i slutet av trådens arbete. Detta anrop blockerar tråden på obestämd tid och bearbetar händelser. För att stoppa, använd CFRunLoopStop(CFRunLoopGetCurrent()). Viktigt: RunLoop.current skapar lat en RunLoop vid första åtkomst — om du inte anropar run(), kommer den inte att bearbeta händelser. Mönster: konfiguration av källor -> lägg till i RunLoop -> anropa run().

swift
import Foundation

// Bakgrundstråd med egen RunLoop
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // RunLoop skapas automatiskt vid anrop av RunLoop.current
            let runLoop = RunLoop.current

            // Lägger till en port för att hålla RunLoop aktiv
            runLoop.add(Port(), forMode: .default)

            // Startar bearbetning av händelser
            var isFinished = false
            while !isFinished {
                // run(mode:before:) returnerar true om en händelse bearbetades
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

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

    func stop() {
        // Stoppa RunLoop på bakgrundstråden
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

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

// Användning: timer på bakgrunds-RunLoop
let manager = BackgroundRunLoopManager()
manager.start()

// Skicka uppgift till bakgrunds-RunLoop via performSelector
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager skapar en bakgrundstråd med permanent RunLoop. Att lägga till en tom Port() är nödvändigt för att förhindra att RunLoop avslutas omedelbart — utan källor returnerar RunLoop.run() false och avslutas. performSelector:onThread: skickar ett meddelande till bakgrunds-RunLoop — det kommer att bearbetas när RunLoop går in i BeforeSources-fasen. Stop anropar CFRunLoopStop på bakgrundstråden och avslutar cykeln.

NSTimer skapar en timerhändelse som RunLoop bearbetar i BeforeTimers-fasen. Timers kan vara repeating (återkommande) och non-repeating (engångs). NSTimer garanterar inte precision: om RunLoop blockeras av en lång operation aktiveras timern efter avblockering, och alla missade aktiveringar slås samman till en (för återkommande timer — högst en ”ikappnings“ aktivering).

CADisplayLink — en specialiserad timer synkroniserad med skärmens uppdateringsfrekvens (60/120/144 Hz). Den används för animeringar och videouppdateringar. CADisplayLink läggs till i RunLoop och aktiveras före varje renderingsram (innan Core Animation skickar lagret till rendering). Om en ram missas (display link inte kunde aktiveras inom 16 ms), sker nästa anrop i nästa VSync-cykel.

swift
import UIKit

class AnimationController {

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

    // CADisplayLink — animering med vsync
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // Lägg till i .common-läge — fungerar även under scrollning
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // Anropas varje bildruta (60 FPS → var 16,6 ms)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // stoppa efter 5 sekunder
        }
    }

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

        // NYCKEL: lägg till i .common, annars fryser timern vid scrollning
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

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

I AnimationController läggs CADisplayLink till i .common-läge, vilket garanterar att step anropas på varje ram oavsett scrollning. displayLink.add(to: .current, forMode: .common) — standardmönstret för animeringar som inte bör avbrytas under scrollning. NSTimer lades också till i .common-läge för att ticka under scrollning. Utan detta skulle timern bara fungera i .default-läge.

RunLoop Observers: övervakning av händelseloopen

CFRunLoopObserver — mekanism för att spåra RunLoop-faser. Med Observer kan du få meddelanden om inträde i läge, start av timerbearbetning, start av källbearbetning, uppvaknande ur sömn, utgång ur läge. Observers används av ramverk för deras behov: Core Animation använder dem för att rendera lager före RunLoop-sömn, UIKit — för att uppdatera layout efter bearbetning av händelser.

Utvecklaren kan också lägga till Observers för egna ändamål. Till exempel: mätning av bearbetningstid för händelser (profilering), utförande av uppskjutna operationer före RunLoop-sömn (när UI redan är uppdaterat och användaren inte interagerar), automatisk sparande av data vid lång inaktivitet. Observer registreras via CFRunLoopAddObserver med angivande av läge och bitmask för övervakade aktiviteter.

Mest användbara punkter för Observer: .afterWaiting — utförs efter att RunLoop vaknat och kan innehålla kod som ska köras efter bearbetning av en händelse; .beforeTimers — före timerbearbetning, gör det möjligt att mäta tid sedan föregående bearbetning; .exit — aktiveras när RunLoop stoppas, användbart för att rensa resurser i bakgrundstråden.

CFRunLoopStop och avslutning av cykeln

CFRunLoopStop — funktion som tvångsavslutar den aktuella RunLoop-iterationen. När CFRunLoopStop(CFRunLoopGetCurrent()) anropas avslutar RunLoop bearbetningen av den aktuella händelsen och lämnar run() och returnerar false. Detta är standardsättet att stoppa RunLoop på en bakgrundstråd. På Main Thread rekommenderas inte CFRunLoopStop — huvud-RunLoop bör fungera under hela appens livslängd. För bakgrundstrådar kan tråden efter CFRunLoopStop avslutas eller fortsätta exekvera nästa kod efter run().

Vanliga frågor

Vad är RunLoop i iOS?

RunLoop — händelseloopen i iOS implementerad av CFRunLoop (Core Foundation) och NSRunLoop (Foundation). Den väntar på händelser (beröringar, timers, inmatningskällor) och skickar dem till hanterare på tråden. Varje tråd kan ha en RunLoop, men automatiskt skapas den endast för Main Thread. RunLoop hanterar lägen (.default, .tracking, .common) och isolerar bearbetning efter prioritet.

Varför fungerar inte NSTimer under scrollning?

NSTimer läggs som standard till i RunLoops .default-läge. När användaren scrollar växlar RunLoop till .tracking-läge och bearbetar inte timers från .default. Lösning: lägg till timern i .common-läge via RunLoop.current.add(timer, forMode: .common). .common kombinerar .default och .tracking, så timern fungerar i båda lägena.

Måste jag starta RunLoop i en bakgrundstråd?

Endast om bakgrundstråden använder timers (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream eller Source-händelser. Om tråden utför en synkron uppgift (filnedladdning, beräkningar) och avslutas — behövs ingen RunLoop. För att starta, anropa RunLoop.current.run() efter konfiguration av källor. För att stoppa — CFRunLoopStop(CFRunLoopGetCurrent()).

Vad skiljer RunLoop från GCD DispatchQueue?

RunLoop fungerar på trådnivå och bearbetar händelser sekventiellt med stöd för lägen (modes). DispatchQueue — en abstraktion av en trådpool, uppgifter utförs på valfri ledig tråd. GCD stöder inte lägen och existerar oberoende av RunLoop. DispatchQueue.main använder huvud-RunLoop för att exekvera block — detta är den enda skärningspunkten. För bakgrundsuppgifter föredras GCD.

Hur är CADisplayLink kopplad till RunLoop?

CADisplayLink — en timer synkroniserad med VSync (skärmens uppdateringsfrekvens). Den läggs till i RunLoop och aktiveras före varje renderingsram i BeforeTimers-fasen. CADisplayLink fungerar bara på Main Thread, eftersom skärmrendering sker där. För kontinuerliga animeringar under scrollning, lägg till den i .common-läge: displayLink.add(to: .current, forMode: .common).

Sammanfattning

  • RunLoop — iOS event loop: bearbetar beröringar, timers, inmatningskällor på tråden; Main Thread har automatisk RunLoop
  • Tre lägen: .default (allmänt), .tracking (scrollning), .common (default + tracking) — hanterar filtrering av händelser
  • NSTimer i .default fungerar inte vid scrollning — lösning: lägg till i .common-läge via RunLoop.current.add(timer, forMode: .common)
  • Bakgrundstrådar har ingen RunLoop som standard — för timers och performSelector: krävs manuell start via RunLoop.current.run()
  • CADisplayLink — timer för varje VSync-ram, nödvändig för jämna animeringar; läggs till i .common för att fungera vid scrollning
  • RunLoop Observer — övervakning av faser: Entry, BeforeTimers, BeforeSources, AfterWaiting, Exit; används för profilering
  • RunLoop ≠ Looper: iOS RunLoop stöder lägen och timers, Android Looper är enklare — utan lägen, Handler + MessageQueue

Vi utvecklar en mobil applikation nyckelfärdigt

IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.

Diskutera projektet

Läs också