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 — ä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.
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.
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).
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 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äge | Core Foundation-konstant | Foundation-konstant | När aktivt |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Normalt tillstånd, utan scrollning |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Scrollning, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-läge: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Första starten av RunLoop |
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å.
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().
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.
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.
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 — 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
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.
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.
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()).
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.
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
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.
Läs också