@MainActor е глобален актьор в езика Swift, който гарантира изпълнението на код на главната нишка. Според Apple Developer, 2024, @MainActor автоматизира преключването към главната нишка при работа с UI, като освобождава разработчика от ръчно извикване на DispatchQueue.main.async. Анотацията се появи в Swift 5.5 заедно с системата async/await.
Основни поенти
@MainActor е глобален актьор (global actor) в Swift, който съчетава свойствата на актьорите с гаранция за изпълнение на главната нишка на приложението. То е част от системата за конкурентност на Swift, представен в Swift 5.5 заедно с async/await и структурирана конкурентност. Анотацията позволява на разработчика да не мисли за ръчно преключване на нишките и намалява броя на UI грешките.
Актьорът в Swift е референтен тип, който изолира състоянието си и гарантира, че само една нишка може да го променя. @MainActor е специален глобален актьор, чийто изпълнител е главната нишка. Всяк код, маркиран с @MainActor, се изпълнява на главната нишка — дори ако е бил извикан от задача на заден план.
Преди появата на @MainActor, разработчиците ръчно преключваха към главната нишка чрез DispatchQueue.main.async. Това беше източник на чести грешки: разработчиците забравяха да преключат, което водеше до крашове поради актуализиране на UI на неглавната нишка. @MainActor решава този проблем на ниво система от типове.
Източникът на повечето грешки в iOS приложения е UI-несигурността — актуализиране на интерфейса от задна нишка. Apple вгради @MainActor в Swift Concurrency, за да направи преключването към главната нишка автоматично и проверяемо от компилатора, елиминирайки цял клас от runtime грешки.
Принципът на действие на @MainActor се базира на изпълнителната система на Swift Concurrency. Когато една нишка извика функция, маркирана с @MainActor, планировшикът го спира на текущия изпълнител и го подновява на главната нишка. Компилаторът проследява границите на извикването и гарантира сигурност.
За изпълнението на @MainActor отговаря MainActor.shared — изпълнителът, свързан с главната нишка на приложението. Когато асинхронна функция е маркирана с @MainActor, тя винаги се подновява на този изпълнител, независимо от това на коя нишка е стартирала първоначалната задача.
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // безопасно, MainActor гарантира главната нишка
}
}
Ако функция е маркирана с @MainActor и извика друга асинхронна функция, по подразбиране тя наслява контекста на актьора. Това означава, че всички вложени извиквания се изпълняват също на главната нишка, освен ако не е посочено друго. Компилаторът проследява това и дава грешка при опит за предаване на несъвместимо затваране.
Сравнението на @MainActor и DispatchQueue.main помага да разберем защо новият механизъм се счита за по-сигурен и по-удобен, въпреки че и двата решават една и съща задача — изпълнение на код на главната нишка.
@MainActor е проверка на ниво компилатор. Ако опитате да извикате @MainActor функция от несигурно контекст, компилаторът ще даде предупреждение или грешка. DispatchQueue.main.async е runtime извикване: кодът ще се компилира, но може да се крашне при работа при опит за актуализиране на UI от задна нишка.
DispatchQueue.main.async добавя блок в опашката, който може да се изпълни с закъснение. @MainActor с async/await извършва пряко преключване на изпълнителя, без да създава допълнителни затварания. Това намалява натоварането и прави кода по-предсказуем по отношение на времето за изпълнение.
// Стар подход
DispatchQueue.main.async {
self.updateUI()
}
// Нов подход с @MainActor
@MainActor
func updateUI() {
// се изпълнява на главната нишка
self.label.text = "Актуализирано"
}
| Критерий | @MainActor | DispatchQueue.main |
|---|---|---|
| Проверка | компилатор | runtime |
| Синтаксис | анотация (декларативен) | извикване (императивно) |
| Натоваране | ниско (преключване на изпълнител) | средно (затваране + опашка) |
| Тестване | високо (MainActor.shared може да бъде заменен) | ниско (трудно за mockване) |
В реални iOS проекти, @MainActor се прилага в ViewModel слоевете, SwiftUI изгледи и UIKit контролерите. Анотацията може да се приложи както на отделни методи, така и на целия тип.
Като маркирате клас с @MainActor, гарантирате, че всичките му методи и свойства са достъпни само на главната нишка. Това е особено удобно за SwiftUI изгледи и ObservableObject класове: просто добавяте @MainActor пред class и всички @Published свойства се актуализират безопасно.
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
При работа със стар UIKit код, където преключването на нишките е било ръчно, може да се използва MainActor.run за явно преключване. Това е удобно за постепен преход към Swift Concurrency, без да се препишва цялата кодова база.
await MainActor.run {
self.tableView.reloadData()
}
Въпреки всички предимства, @MainActor има редица ограничения, които трябва да се вземат предвид при проектирането на архитектурата на приложението. Разбирането на границите на приложимост помага да се избегне неправилната употреба.
Ако цялата верига от извиквания е маркирана с @MainActor, всяка тежка работа ще се изпълнява на главната нишка, причинявайки замразване на UI. Препоръчва се да маркирате с @MainActor само UI слоя, а бизнес логиката и мрежовите заявки да останат в актьорите на заден план или глобалния изпълнител.
Старите API, базиращи се на callback (например, URLSession без async/await), не поддържат контекста на актьора. За интеграция се изисква обвиване с CheckedContinuation. Също така, @MainActor не е съвместим с performSelector, target-action и други неасинхронни модели на UIKit.
При отладването на приложения с @MainActor е по-трудно да се възпроизведат състояния на надпреварване, защото компилаторът предотвратява много от тях в фазата на изграждане, а не при работа. Това обаче може да създаде фалшиво усещане за сигурност: неправилната работа с общи mutable обекти (например, NSCache или общи глобални променливи) е все още възможна, ако не са маркирани с @MainActor и се използват без явна синхронизация.
@MainActor значително опростява тестването на UI логиката, тъй като елиминира необходимостта от ръчно преключване на нишките в тестовете. Съществуват обаче характеристики, които трябва да се вземат предвид при писането на единични тестове и UI тестове.
В XCTest, изпълнителната среда автоматично настроява изпълнителя на главната нишка. Когато тестовият метод се изпълнява на главната нишка, извикването на @MainActor функции не изисква допълнителна конфигурация — те се изпълняват в същия контекст. За тестване на сценарии на заден план, използвайте MainActor.run внутрено в Task с явно посочване на приоритет и изпълнител, като отделно проверявате, че кодът работи правилно при извикване от заден план.
Един от обичайните подходи е тестването на ViewModel с @MainActor, където се проверява, че @Published свойствата се актуализират правилно след асинхронни операции. Благодарение на наследяването на контекста на актьора, извикването await внутрено в теста гарантира изпълнение на главната нишка без допълнителни DispatchQueue гаранции и явно преключване на контекста, което опростява писането на тестовете.
При рефакториране на съществуващ код към Swift Concurrency, проверявайте изолацията @MainActor чрез компилатора: всяко извикване на синхронни методи без @MainActor от @MainActor контекст се маркира като грешка. Това свойство се използва за постепен преход на проекта към async/await: маркирате ViewModel слоя като @MainActor и компилаторът подсветлява всички несигурни извиквания, които трябва да се преместят в актьорите на заден план.
При създаването на mockове за @MainActor зависимости, използвайте протоколи с async методи, които декларират асинхронни функции с възвращаеми типове. Това позволява замяна на мрежови услуги, бази от данни и други външни зависимости, без да се нарушава актьорната изолация. Компилаторът ще провери дали mockът отговаря на всички изисквания за изолация, предотвратявайки случаен достъп до @MainActor код от тестови нишки на заден план.
При синхронно тестване на @MainActor код, използвайте XCTestExpectation за изчакване на завършването на асинхронните операции. Поставете очакването в теста и изпълнете fulfillment внутрено в затварането, което се изпълнява на главната нишка. Ако тестът висне безкрайно — вероятно извикването на главната нишка не се случва и трябва да се провери актьорната изолация. За отладване на контекста на изпълнение, полезно е да добавите проверка Thread.isMainThread внутрено в тестовия код.
Често задавани въпроси
Не, достатъчно е да маркирате само методите, които актуализират UI. Ако обаче има няколко такива метода в класа, по-лесно е да добавите @MainActor на целия клас. Това гарантира, че всичките му членове се изпълняват на главната нишка и опростява поддръжката на кода.
@MainActor е конкретен инстанс на глобалния актьор, свързан с главната нишка. @globalActor е протокол за създаване на собствени глобални актьори. Например, можете да създадете @BackgroundActor за изпълнение на код на задна нишка, ако архитектурата на проекта изисква това.
Да, синхронните функции с @MainActor също се изпълняват на главната нишка. Основната стойност на @MainActor се разкрива именно с async/await, когато асинхронната функция автоматично се подновява на главната нишка без ръчно преключване чрез DispatchQueue.main.
Task.cancel() работи с @MainActor задачи по същия начин като с обикновените. @MainActor задачата може да провери Task.isCancelled или да хвърли CancellationError. При отказване главната нишка не се блокира — задачата просто спира изпълнение на най-близката точка на спиране.
Компилаторът гарантира сигурност: ако извикате @MainActor функция от заден контекст, компилаторът ще посочи грешката. За асинхронни извиквания е достатъчно да маркирате извикващия код с await, и изпълнителят сам ще преключи към главната нишка. За синхронни извиквания се изисква явно преключване чрез MainActor.run.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също