MainActor — это новый атрибут, представленный в Swift 5.5 как глобальный Actor, предоставляющий исполнителя, который выполняет свои задачи в основном потоке. При создании приложений важно выполнять задачи обновления пользовательского интерфейса в основном потоке, что иногда может быть затруднительно при использовании нескольких фоновых потоков. Использование атрибута @MainActor поможет вам убедиться, что ваш пользовательский интерфейс всегда обновляется в основном потоке.
Если вы новичок в работе с actor в Swift, я рекомендую прочитать мою статью actors в Swift: как использовать и предотвращать гонки данных. Глобальные акторы действуют так же, как акторы, и я не буду вдаваться в подробности того, как работают акторы в этом посте.
Что такое MainActor?
MainActor — глобально уникальный actor, который выполняет свои задачи в основном потоке. Его следует использовать для свойств, методов, экземпляров и замыканий для выполнения задач в основном потоке. Предложение SE-0316 Global Actors представило главного субъекта как пример глобального субъекта, и он наследует протокол GlobalActor.
Понимание Global actors
GlobalActor можно рассматривать как синглтоны: у каждого есть только один экземпляр. На данный момент GlobalActors работают только за счет экспериментального параллелизма. Вы можете сделать это, добавив следующее значение в «Other Swift Flags» в настройках сборки Xcode:
-Xfrontend -enable-experimental-concurrency
Один включенный, мы могли бы определить нашего собственного GlobalActor следующим образом:
@globalActor
actor SwiftLeeActor {
static let shared = SwiftLeeActor()
}
Общее свойство является требованием протокола GlobalActor и гарантирует наличие глобально уникального экземпляра субъекта. После определения вы можете использовать GlobalActor в своем проекте так же, как и с другими actors.
@SwiftLeeActor
final class SwiftLeeFetcher {
// ..
}
Как использовать MainActor в Swift?
Global actor может использоваться со свойствами, методами, замыканиями и экземплярами. Например, мы могли бы добавить атрибут Main Actor в модель представления, чтобы она выполняла все свои задачи в основном потоке:
@MainActor
final class HomeViewModel {
// ..
}
Используя nonisolated, мы можем убедиться, что методы, не требующие основного потока, работают максимально быстро. Класс может быть аннотирован Global actor только в том случае, если у него нет суперкласса, суперкласс аннотирован тем же Global actor или суперклассом является NSObject. Подкласс класса, аннотированного Global actor, должен быть изолирован от одного и того же Global actor.
В других случаях мы можем захотеть определить отдельные свойства с помощью Global actor:
final class HomeViewModel {
@MainActor var images: [UIImage] = []
}
Пометка свойства images с помощью свойства @MainActor гарантирует, что его можно будет обновить только из основного потока:
Отдельные методы также могут быть отмечены атрибутом:
@MainActor func updateViews() {
// Perform UI updates..
}
И даже замыкания могут быть помечены для выполнения в основном потоке:
func updateData(completion: @MainActor @escaping () -> ()) {
/// Example dispatch to mimic behaviour
DispatchQueue.global().async {
async {
await completion()
}
}
}
Использование MainActor напрямую
MainActor в Swift поставляется с расширением для прямого использования актора:
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
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 непосредственно из методов, даже если мы не определяли ни одно из его тел с помощью атрибута Global actor:
async {
await MainActor.run {
// Perform UI updates
}
}
Другими словами, больше нет необходимости использовать DispatchQueue.main.async.
Когда следует использовать атрибут MainActor?
До Swift 5.5 вы могли определить множество операторов диспетчеризации, чтобы убедиться, что задачи выполняются в основном потоке. Пример может выглядеть следующим образом:
func fetchData(completion: @escaping (Result<[UIImage], Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "..some URL")) { data, response, error in
// .. Decode data to a result
DispatchQueue.main.async {
completion(result)
}
}
}
В приведенном выше примере мы почти уверены, что диспетчеризация необходима. Однако в других случаях отправка может быть ненужной, поскольку мы уже находимся в главном потоке. Это привело бы к дополнительной отправке, которую можно было бы пропустить.
В любом случае, в этих случаях имеет смысл определить свойства, методы, экземпляры или замыкания в качестве основного актера, чтобы убедиться, что задачи выполняются в основном потоке. Мы могли бы, например, переписать приведенный выше пример следующим образом:
func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in
// .. Decode data to a result
let result: Result<[UIImage], Error> = .success([])
async {
await completion(result)
}
}
}
Поскольку сейчас мы работаем с замыканием, определяемым субъектом, нам нужно использовать технику асинхронного ожидания для вызова нашего замыкания. Использование здесь атрибута @MainActor позволяет компилятору Swift оптимизировать наш код для повышения производительности.
Выбор правильной стратегии
Важно выбрать правильную стратегию с actors. В приведенном выше примере мы решили сделать замыкание actor, а это значит, что кто бы ни использовал наш метод, обратный вызов завершения будет выполняться с помощью MainActor. В некоторых случаях это может не иметь смысла, если метод запроса данных также используется из места, где не важно обрабатывать обратный вызов завершения в основном потоке.
В таких случаях лучше возложить ответственность за отправку в правильную очередь на разработчиков:
viewModel.fetchData { result in
async {
await MainActor.run {
// Handle result
}
}
}
Продолжаем свое путешествие в Swift Concurrency
Изменения параллелизма — это больше, чем просто async-await, они включают в себя множество новых функций, которые вы можете использовать в своем коде. Итак, пока вы этим занимаетесь, почему бы не погрузиться в другие функции параллелизма?
- Tasks in Swift explained with code examples
- Async await in Swift explained with code examples
- Sendable and @Sendable closures explained with code examples
- Nonisolated and isolated keywords: Understanding Actor isolation
- Async let explained: call async functions in parallel
- MainActor usage in Swift explained to dispatch to the main thread
- Actors in Swift: how to use and prevent data races
Заключение
Глобальные actors — отличное дополнение к actors в Swift. Это позволяет нам повторно использовать общих actors и позволяет эффективно выполнять задачи пользовательского интерфейса, поскольку компилятор может оптимизировать наш код внутри. Глобальные actors можно использовать для свойств, методов, экземпляров и замыканий, после чего компилятор обеспечивает соблюдение требований в нашем коде.
Если вы хотите узнать больше советов по Swift, посетите страницу категории Swift. Не стесняйтесь обращаться ко мне или писать мне в Твиттере, если у вас есть какие-либо дополнительные советы или отзывы.
Спасибо!