Swift MainActor 的使用教程

前言

在我们的项目开发过程中,经常会碰到多层闭包嵌套的情况,如果闭包又涉及到网络接口的调用,那我们需要时刻谨记对于回调中关于视图页面相关的操作要在主线程调用。

但不幸的是,这很容易被忽略掉,我就经常忘记回到主线程。咔咔写完一运行,项目崩溃了,一看原因才想起来没有回到主线程。😂

现在我们不需要时刻想着这事了,因为官方替我们解了忧。它就是本文的主题:MainActor

MainActor 简介

MainActor 是 Swift 5.5 中引入的一个新特性。它是一个全局actor,提供了一个在主线程上执行任务的执行器。使用 @MainActor可以确保你的视图页面的相关代码总是在主线程上调用。

下面是 MainActor 的源码:

swift 复制代码
@globalActor 
final actor MainActor: GlobalActor {
    static let shared: MainActor 
}

首先,我们先来了解一下 @globalActor。下面是其源码:

csharp 复制代码
public protocol GlobalActor {
    associatedtype ActorType : Actor
    global actor type.
    static var shared: Self.ActorType { get }
}

通过代码可以看到,它是一个协议,如果我们遵守该协议,需要实现一个单例的静态属性。比如下面的这个例子:

scss 复制代码
@globalActor
actor CustomActor { // 自定义的 actor
    static let shared = CustomActor()
}

单例常量 shared 确保全局只有唯一一个实例。一旦定义,你就可以在整个项目中使用全局 actor,就像使用其他 actor 一样:

kotlin 复制代码
@CustomActor
class CustomClass { }

MainActor 如何使用

MainActor 可以修饰属性、函数、闭包和实例变量。假设,对某个函数的调用我们想始终放在主线程,就可以像下面这么用:

less 复制代码
@MainActor
func updateUIOperation() {
    // 进行 UI 操作
}

除了使用 MainActor 修饰,我们也可以直接使用其 extension 中提供的 run 函数来进行使用:

less 复制代码
extension MainActor {
    public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T where T : Sendable
}

这允许我们直接在函数中使用 MainActor,即使我们没有使用全局 actor 来修饰任何代码:

arduino 复制代码
Task {
    await fetchListData()
    await MainActor.run {
        // 进行 UI 操作
    }
}

所以,以后我们就没必要再使用 DispatchQueue.main.async 来进行队列切换了。对于 MainActor 的这两种使用方式,我个人比较推荐第一种,因为对于第二种调用 run 函数这种方式,很可能会写着写着就忘了,从而发生视图页面的更新代码在后台线程执行的情况。

使用 MainActor 重构以前的代码

在 Swift 5.5 之前,如果编写队列切换的代码,你可能会使用下面这种方式:

less 复制代码
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()
}

在上面的示例中,如果请求失败,我们需要回到主线程将错误返回,如果请求成功我们也需要回到主线程返回 image。这就导致了几个闭包的代码混乱。

通过使用 MainActor 和 async/await 的搭配,我们可以将上面的例子重构成下面的代码:

swift 复制代码
@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 确保逻辑在主线程上执行,而网络请求仍然在后台队列上执行。只有在需要确保最佳性能时才会调度到 MainActor。

相关推荐
大龄秃头程序员2 天前
Swift Concurrency 取消机制踩坑复盘:为什么很多人会误以为 Task 根本取消不了?
swift
做前端的娜娜子3 天前
如何实现网页加载进度条?
性能优化·掘金·金石计划
做前端的娜娜子4 天前
大文件上传
掘金·金石计划
做前端的娜娜子4 天前
React 性能优化
掘金·金石计划
做前端的娜娜子5 天前
Vue的响应式原理
掘金·金石计划
软泡芙5 天前
【IOS】 Swift Package Manager (SPM) 指南
网络·ios·swift
东坡肘子5 天前
不是模型变慢了,是任务变大了 -- 肘子的 Swift 周报 #146
人工智能·swiftui·swift
大龄秃头程序员6 天前
Sendable 不是“能跑就行”:Swift Concurrency 下的实战踩坑与架构落地指南
swift
做前端的娜娜子6 天前
Web缓存策略有哪些?
掘金·金石计划