MainActor — это новый атрибут, представленный в Swift 5.5 как глобальный актор, предоставляющий исполнителя, который выполняет свои задачи в основном потоке. При создании приложений важно выполнять задачи обновления пользовательского интерфейса в основном потоке, что иногда может быть затруднительно при использовании нескольких фоновых потоков. Использование атрибута @MainActor поможет убедиться, что ваш пользовательский интерфейс всегда обновляется в основном потоке.
Если вы новичок в работе с Actor в Swift, я рекомендую прочитать мою статью Actors в Swift: как использовать и предотвращать гонки данных. Глобальные акторы действуют так же, как actors, и я не буду вдаваться в подробности того, как работают Actors в этом посте.
Что такое MainActor?
MainActor — глобально уникальный актор, который выполняет свои задачи в основном потоке. Вы можете использовать его для свойств, методов, экземпляров и замыканий для выполнения задач в основном потоке. Предложение SE-0316 «Global Actors» представило главного действующего лица в качестве примера глобального действующего лица, наследующего протокол GlobalActor.
Понимание глобальных участников
Прежде чем мы углубимся в то, как использовать MainActor в вашем коде, важно понять концепцию глобальных акторов. Вы можете видеть Global Actors как синглтоны: существует только один экземпляр. Мы можем определить глобального актора следующим образом:
@globalActor
actor SwiftLeeActor {
static let shared = SwiftLeeActor()
}
Общее свойство является требованием протокола GlobalActor и гарантирует наличие глобально уникального экземпляра субъекта. После определения вы можете использовать глобального актера в своем проекте так же, как и с другими актерами:
@SwiftLeeActor
final class SwiftLeeFetcher {
// ..
}
Везде, где вы используете глобальный атрибут actor, вы обеспечите синхронизацию через общий экземпляр actor, чтобы обеспечить взаимоисключающий доступ к объявлениям. Результат аналогичен акторам в целом, как описано в разделе «Актеры в Swift: как использовать и предотвращать гонки данных».
Базовая реализация @MainActor аналогична нашей пользовательской реализации @SwiftLeeActor:
@globalActor
final actor MainActor: GlobalActor {
static let shared: MainActor
}
Он доступен по умолчанию и определен внутри среды параллелизма. Другими словами, вы можете сразу же начать использовать этот глобальный актор и пометить свой код для выполнения в основном потоке, синхронизировав его через этот глобальный актор.
Как использовать MainActor в Swift?
Вы можете использовать глобальный актор со свойствами, методами, замыканиями и экземплярами. Например, мы могли бы добавить атрибут @MainActor в модель представления, чтобы выполнять все задачи в основном потоке:
@MainActor
final class HomeViewModel {
// ..
}
Используя nonisolated методы, мы гарантируем максимально быстрое выполнение методов, не требующих основного потока, не дожидаясь, пока основной поток станет доступным. Вы можете аннотировать класс с глобальным актором только в том случае, если у него нет суперкласса, суперкласс аннотирован тем же глобальным актором или суперклассом является NSObject. Подкласс класса, аннотированного глобальным субъектом, должен быть изолирован от одного и того же глобального субъекта.
В других случаях мы можем захотеть определить отдельные свойства с глобальным актором:
final class HomeViewModel {
@MainActor var images: [UIImage] = []
}
Пометка свойства images с помощью свойства @MainActor гарантирует, что его можно будет обновить только из основного потока:
Вы также можете пометить атрибутом отдельные методы:
@MainActor func updateViews() {
// Perform UI updates..
}
И вы даже можете пометить замыкания для выполнения в основном потоке:
func updateData(completion: @MainActor @escaping () -> ()) {
Task {
await someHeavyBackgroundOperation()
await completion()
}
}
Хотя в этом случае вам следует переписать метод updateData на асинхронный вариант без необходимости замыкания завершения.
Использование главного актера напрямую
MainActor в Swift поставляется с расширением для прямого использования актора:
extension MainActor {
/// Execute the given body closure on the main actor.
public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}
Это позволяет нам использовать MainActor непосредственно из методов, даже если мы не определяли ни одно из его тел с помощью атрибута глобального актора:
Task {
await someHeavyBackgroundOperation()
await MainActor.run {
// Perform UI updates
}
}
Другими словами, больше нет необходимости использовать DispatchQueue.main.async. Однако я рекомендую использовать глобальный атрибут, чтобы ограничить любой доступ к основному потоку. Без атрибута глобального актора любой может забыть использовать MainActor.run, что может привести к обновлению пользовательского интерфейса в фоновом потоке.
Когда следует использовать атрибут MainActor?
До Swift 5.5 вы могли определить множество операторов диспетчеризации, чтобы гарантировать, что задачи выполняются в основном потоке. Пример может выглядеть следующим образом:
func fetchImage(for url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data, let image = UIImage(data: data) else {
DispatchQueue.main.async {
completion(.failure(ImageFetchingError.imageDecodingFailed))
}
return
}
DispatchQueue.main.async {
completion(.success(image))
}
}.resume()
}
В приведенном выше примере вы уверены, что для возврата изображения в основной поток необходима диспетчеризация. Нам приходится выполнять диспетчеризацию в нескольких местах, что приводит к беспорядку в коде из-за нескольких замыканий.
Иногда мы могли бы даже отправить в основную очередь, уже находясь в основном потоке. Такой случай привел бы к дополнительной отправке, которую вы могли бы пропустить. Переписывая свой код для использования async/await и основного актера, вы разрешаете оптимизацию диспетчеризации только в случае необходимости.
В этих случаях изоляция свойств, методов, экземпляров или замыканий для основного субъекта гарантирует выполнение задач в основном потоке. В идеале мы бы переписали приведенный выше пример следующим образом:
@MainActor
func fetchImage(for url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageFetchingError.imageDecodingFailed
}
return image
}
Атрибут @MainActor обеспечивает выполнение логики в основном потоке, в то время как сетевой запрос все еще выполняется в фоновой очереди. Отправка главному актеру происходит только в случае необходимости для обеспечения наилучшей производительности.