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。

相关推荐
东坡肘子4 小时前
参与学生移动应用大赛有感 -- 肘子的 Swift 周报 #161
人工智能·swiftui·swift
songgeb5 天前
YYImage Bug:一行之差,正常 JPEG 如何在 iOS 被读成条纹
ios·swift
fthux5 天前
Tick Rest,一个休息提醒工具的自我修养
macos·开源·swift
Fatbobman(东坡肘子)6 天前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
开发语言·人工智能·redis·ios·swiftui·iphone·swift
chaoahyl6 天前
WaterDiary 喝水记录 App 实战(2)· 实现喝水首页与本地保存
swift
茶底世界之下7 天前
iPhone Duo 适配实战,Awaking 踩过的 8 个坑
ios·swiftui·swift
xbzb7 天前
Swift 对象存储建环实验 + Ceilometer 计量:两个收尾服务
swift·ring·实验·ceilometer·象存储·建环
2501_916008897 天前
iOS App 图标、启动屏和多语言搭配 方法,上架前使用KXBuild一次做好 App 图标
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
2501_915918417 天前
iOS 推送通知怎么实现,从 APNs p8 密钥到 iOS 真机收到第一条推送
vscode·ios·objective-c·个人开发·swift·敏捷流程
东坡肘子7 天前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
人工智能·swiftui·swift