Swift 中的 MainActor 使用和主线程调度

MainActor 是Swift 5.5中引入的一个新属性,它是一个全局 actor,提供一个在主线程上执行任务的执行器。在构建应用程序时,在主线程上执行UI更新任务是很重要的,在使用几个后台线程时,这有时会很有挑战性。使用@MainActor属性将帮助你确保你的UI总是在主线程上更新。

如果您不熟悉 Swift 中的 Actors,我建议您阅读我的文章Swift中的Actors 使用以如何及防止数据竞争,全局Actors的行为类似于Actors,我不会在这篇文章中详细介绍Actors的工作方式。

什么是 MainActor?

MainActor 是一个全局唯一的 Actor,他在主线程上执行他的任务。它应该被用于属性、方法、实例和闭包,以在主线程上执行任务。提案SE-0316 全局Actor 引入了 MainActor,作为其全局 Actor 的一个例子,它继承了GlobalActor协议。

理解全局 Actors

全局 Actor 可以看作是单例:每个只有一个实例。如果你的Xcode不支持,请升级到最新版本或者通过启用实验并发来工作。您可以通过在 Xcode 的构建设置中将以下值添加到“Other Swift Flags”中来实现:

-Xfrontend -enable-experimental-concurrency

我们可以定义我们自己的全局 Actor 如下:

@globalActor
actor SwiftLeeActor {
    static let shared = SwiftLeeActor()
}

共享属性是GlobalActor协议的一个要求,它可以确保有一个全球唯一的角色实例。一旦被定义,你就可以在整个项目中使用全局Actor,就像你对其他 Actor 一样:

@SwiftLeeActor
final class SwiftLeeFetcher {
    // ..
}

如何在 Swift 中使用 MainActor

全局actor可以与属性、方法、闭包和实例一起使用。例如,我们可以将 MainActor属性添加到视图模型中,以使其在主线程上执行所有任务:

@MainActor
final class HomeViewModel {
    // ..
}

使用nonisolated,我们可以确保没有主线程要求的方法尽可能快地执行。如果一个类没有父类,父类使用相同的全局actor注释,或者父类是NSObject,则只能使用全局actor进行注释。 全局 Actor 注释的类的子类必须与同一个全局 Actor 隔离。

在其他情况下,我们可能希望使用全局Actor定义单个属性:

final class HomeViewModel {
    
    @MainActor var images: [UIImage] = []

}

@MainActor属性标记images属性,可以确保它只能从主线程更新:

The MainActor attribute requirements are enforced by the compiler.

编译器执行MainActor的属性要求,可使用如下代码修复错误:

final class HomeViewModel {
    @MainActor var images: [UIImage] = []
    func updateImages() async {
        await MainActor.run {
            images = []
        }
    }
}
// OR
final class HomeViewModel {
    @MainActor var images: [UIImage] = []
    @MainActor
    func updateImages() {
        images = []
    }
}

单独的方法也可以用该属性进行标记:

@MainActor func updateViews() {
    // Perform UI updates..
}

甚至可以将闭包标记为在主线程上执行:

func updateData(completion: @MainActor @escaping () -> ()) {
    /// Example dispatch to mimic behaviour
    DispatchQueue.global().async {
        async {
            await completion()
        }
    }
}

直接使用 MainActor

Swift 中的 MainActor 带有一个可以直接使用 Actor 的扩展:

@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,即使我们没有使用全局 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)
        }
    }
}

由于我们现在使用的是一个actor定义的闭包,我们需要使用 async-await 技术来调用我们的闭包。在这里使用@MainActor属性可以让Swift编译器对我们的代码进行性能优化。

选择正确的策略

使用 actors 时选择正确的策略很重要。在上面的例子中,我们决定让闭包成为一个actor,这意味着无论谁使用我们的方法,完成回调都将使用 MainActor 执行。在某些情况下,如果数据请求方法也是从一个不需要在主线程上处理完成回调的地方使用,这可能就没有意义了。

在这些情况下,让实现者负责调度到正确的队列可能会更好。

viewModel.fetchData { result in
    async {
        await MainActor.run {
            // Handle result
        }
    }
}

继续你的Swift并发之旅

并发的变化不仅仅是 async-await,还包括许多新的功能,你可以从你的代码中受益。所以,当你在做这件事的时候,为什么不深入研究一下其他的并发功能呢?

结论

全局Actor是对Swift中的Actor的一个很好的补充。它允许我们重用常见的Actor,并使UI任务的执行成为可能,因为编译器可以在内部优化我们的代码。全局Actor可以用在属性、方法、实例和闭包上,之后编译器会确保要求在我们的代码中得到保证。

转自 MainActor usage in Swift explained to dispatch to the main thread

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容