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。

相关推荐
大熊猫侯佩1 小时前
国内顶级 AI 的回答令人“贻笑大方”:看来苹果秃头码农们暂时还不会失业吧?
ai编程·swift·apple
不二狗5 小时前
每日算法 -【Swift 算法】电话号码字母组合
开发语言·算法·swift
HarderCoder1 天前
ByAI:iOS 生命周期:AppDelegate 与 SceneDelegate 中的 `willEnterForeground` 方法解析
swift
大熊猫侯佩1 天前
SwiftData 如何在 Widgets 和 App 的界面之间同步数据变化?
swiftui·swift·apple watch
YungFan1 天前
SwiftUI-Preference
swiftui·swift
HarderCoder1 天前
ByAI:使用DRY原则编写干净可复用的Swift代码
swift
season_zhu1 天前
Swift:优雅又强大的语法糖——Then库
ios·架构·swift
东坡肘子1 天前
Swift 新设计、新案例、新体验 | 肘子的 Swift 周报 #087
swiftui·swift·wwdc