@MainActor är en global aktör i språket Swift som garanterar att körning sker på huvudtråden. Enligt Apple Developer, 2024 automatiserar @MainActor växlingen till huvudtråden vid arbete med UI, vilket befriar utvecklaren från manuell anropning av DispatchQueue.main.async. Annotationen introducerades i Swift 5.5 tillsammans med async/await-systemet.
Huvudpunkter
@MainActor är en global aktör (global actor) i Swift som kombinerar aktörers egenskaper med garanti för körning på applikationens huvudtråd. Det är en del av Swift-samtidighetssystemet, introducerat i Swift 5.5 tillsammans med async/await och strukturerad samtidighet. Annotationen låter utvecklaren slippa tänka på manuell trådväxling och minskar antalet UI-fel.
En aktör i Swift är en referenstyp som isolerar sitt tillstånd och garanterar att endast en tråd kan ändra det. @MainActor är en speciell global aktör vars utförare är huvudtråden. All kod som markerats med @MainActor körs på huvudtråden — även om den anropats från en bakgrundsuppgift.
Före @MainActor växlade utvecklare manuellt till huvudtråden via DispatchQueue.main.async. Detta var en källa till frekventa fel: utvecklare glömde att växla, vilket ledde till krascher på grund av UI-uppdateringar på en icke-huvudtråd. @MainActor löser detta problem på typsystemets nivå.
Källan till de flesta buggar i iOS-applikationer är UI-osäkerhet — uppdatering av gränssnittet från en bakgrundstråd. Apple inbyggde @MainActor i Swift Concurrency för att göra växlingen till huvudtråden automatisk och kontrollerbar av kompilatorn, vilket eliminerar en hel klass av runtime-fel.
Funktionsprincipen för @MainActor baseras på Swift Concurrency-körningssystemet. När en tråd anropar en funktion markerad med @MainActor, suspenderar schemaläggaren den på den aktuella utföraren och återupptar den på huvudtråden. Kompilatorn övervakar anropsgränserna och garanterar säkerhet.
För körning av @MainActor ansvarar MainActor.shared — utföraren associerad med applikationens huvudtråd. När en asynkron funktion markeras med @MainActor, återupptas den alltid på denna utförare, oavsett vilken tråd den ursprungliga uppgiften startades på.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // säkert, MainActor garanterar huvudtråden
}
}
Om en funktion är markerad med @MainActor och anropar en annan asynkron funktion, ärver den som standard aktörskontexten. Det innebär att alla nästlade anrop också körs på huvudtråden, om inte annat anges. Kompilatorn övervakar detta och ger ett fel vid försök att skicka ett inkompatibelt tillslutningsblock.
Jämförelsen av @MainActor och DispatchQueue.main hjälper till att förstå varför den nya mekanismen anses säkrare och bekvämare, även om båda löser samma uppgift — körning av kod på huvudtråden.
@MainActor är en kontroll på kompilatornivå. Om du försöker anropa en @MainActor-funktion från en osäker kontext, ger kompilatorn en varning eller ett fel. DispatchQueue.main.async är ett runtime-anrop: koden kompileras, men kan krascha vid körning vid försök att uppdatera UI från en bakgrundstråd.
DispatchQueue.main.async lägger till ett block i kön som kan köras med fördröjning. @MainActor med async/await utför en direkt växling av utföraren utan att skapa extra tillslutningsblock. Detta minskar overhead och gör koden mer förutsägbar när det gäller körningstid.
// Gammal metod
DispatchQueue.main.async {
self.updateUI()
}
// Ny metod med @MainActor
@MainActor
func updateUI() {
// körs på huvudtråden
self.label.text = "Uppdaterad"
}
| Kriterium | @MainActor | DispatchQueue.main |
|---|---|---|
| Kontroll | kompilator | runtime |
| Syntax | annotation (deklarativ) | anrop (imperativ) |
| Overhead | låg (utförarväxling) | medel (tillslutning + kö) |
| Testbarhet | hög (MainActor.shared kan bytas ut) | låg (svårt att mocka) |
I verkliga iOS-projekt tillämpas @MainActor i ViewModel-lager, SwiftUI-vyer och UIKit-kontroller. Annotationen kan appliceras både på enskilda metoder och på hela typen.
Genom att markera en klass med @MainActor garanterar du att alla dess metoder och egenskaper endast är tillgängliga på huvudtråden. Detta är särskilt praktiskt för SwiftUI-vyer och ObservableObject-klasser: du lägger helt enkelt till @MainActor före class, och alla @Published-egenskaper uppdateras säkert.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
När du arbetar med äldre UIKit-kod, där trådväxling var manuell, kan du använda MainActor.run för explicit växling. Detta är praktiskt för gradvis övergång till Swift Concurrency utan att skriva om hela kodbasen.
await MainActor.run {
self.tableView.reloadData()
}
Trots alla fördelar har @MainActor ett antal begränsningar som är viktiga att beakta vid design av applikationsarkitekturen. Att förstå tillämpningsgränserna hjälper till att undvika felaktig användning.
Om hela anropskedjan är markerad med @MainActor, kommer allt tungt arbete att utföras på huvudtråden, vilket orsakar UI-frysning. Det rekommenderas att endast markera UI-lagret med @MainActor och lämna affärslogik och nätverksförfrågningar i bakgrundsaktörer eller den globala utföraren.
Äldre callback-baserade API:er (t.ex. URLSession utan async/await) stöder inte aktörskontexten. För integration krävs en inpackning med CheckedContinuation. Dessutom är @MainActor inte kompatibel med performSelector, target-action och andra icke-asynkrona UIKit-mönster.
Vid felsökning av applikationer med @MainActor är det svårare att reproducera race conditions, eftersom kompilatorn förhindrar många av dem i byggfasen, inte vid körning. Detta kan dock skapa en falsk känsla av säkerhet: felaktig hantering av delade muterbara objekt (t.ex. NSCache eller delade globala variabler) är fortfarande möjlig om de inte är markerade med @MainActor och används utan explicit synkronisering.
@MainActor förenklar testning av UI-logik avsevärt, eftersom det eliminerar behovet av manuell trådväxling i tester. Det finns dock egenskaper som måste beaktas vid skrivning av enhetstester och UI-tester.
I XCTest konfigurerar körningsmiljön automatiskt utföraren av huvudtråden. När testmetoden körs på huvudtråden kräver anrop av @MainActor-funktioner ingen ytterligare konfiguration — de körs i samma kontext. För testning av bakgrundsscenarier, använd MainActor.run inom Task med explicit angivande av prioritet och utförare, och kontrollera separat att koden fungerar korrekt när den anropas från bakgrunden.
Ett av de vanligaste tillvägagångssätten är testning av ViewModel med @MainActor, där man kontrollerar att @Published-egenskaper uppdateras korrekt efter asynkrona operationer. Tack vare arv av aktörskontexten garanterar anrop av await inom testet körning på huvudtråden utan ytterligare DispatchQueue-garantier och manuell kontextväxling, vilket förenklar skrivning av tester.
Vid refaktorering av befintlig kod till Swift Concurrency, kontrollera @MainActor-isoleringen via kompilatorn: alla anrop av synkrona metoder utan @MainActor från @MainActor-kontexten markeras som fel. Denna egenskap används för gradvis övergång av projektet till async/await: du markerar ViewModel-lagret som @MainActor, och kompilatorn framhäver alla osäkra anrop som måste flyttas till bakgrundsaktörer.
När du skapar mockar för @MainActor-beroenden, använd protokoll med async-metoder som deklarerar asynkrona funktioner med returtyper. Detta möjliggör utbyte av nätverkstjänster, databaser och andra externa beroenden utan att bryta aktörsisoleringen. Kompilatorn kommer att kontrollera att mocken uppfyller alla isoleringskrav, vilket förhindrar oavsiktlig åtkomst till @MainActor-kod från bakgrunds testtrådar.
Vid synkron testning av @MainActor-kod, använd XCTestExpectation för att vänta på slutförande av asynkrona operationer. Ställ in förväntan i testet och utför fulfillment inom tillslutningsblocket som körs på huvudtråden. Om testet hänger oändligt — sannolikt sker anropet på huvudtråden inte och aktörsisoleringen måste kontrolleras. För felsökning av körningskontexten är det användbart att lägga till Thread.isMainThread-kontroll i testkoden.
Vanliga frågor
Nej, det räcker att markera endast metoderna som uppdaterar UI. Men om det finns flera sådana metoder i klassen är det enklare att lägga till @MainActor på hela klassen. Detta garanterar att alla dess medlemmar körs på huvudtråden och förenklar kodunderhåll.
@MainActor är en konkret instans av den globala aktören, bunden till huvudtråden. @globalActor är ett protokoll för att skapa egna globala aktörer. Du kan till exempel skapa @BackgroundActor för att köra kod på en bakgrundstråd, om projektets arkitektur kräver det.
Ja, synkrona funktioner med @MainActor körs också på huvudtråden. Men det huvudsakliga värdet av @MainActor framträder just med async/await, när den asynkrona funktionen automatiskt återupptas på huvudtråden utan manuell växling via DispatchQueue.main.
Task.cancel() fungerar med @MainActor-uppgifter på samma sätt som med vanliga uppgifter. En @MainActor-uppgift kan kontrollera Task.isCancelled eller kasta CancellationError. Vid avbrytning blockeras inte huvudtråden — uppgiften slutar helt enkelt köras vid närmaste uppehållspunkt.
Kompilatorn garanterar säkerhet: om du anropar en @MainActor-funktion från en bakgrundskontext, kommer kompilatorn att påpeka felet. För asynkrona anrop räcker det att markera den anropande koden med await, och utföraren växlar själv till huvudtråden. För synkrona anrop krävs explicit växling via MainActor.run.
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å