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 天前
继承、Lambda、Objective-C和Swift
开发语言·macos·ios·objective-c·swift·继承·lambda
KWMax2 天前
RxSwift系列(二)操作符
ios·swift·rxswift
Mamong2 天前
Swift并发笔记
开发语言·ios·swift
小溪彼岸2 天前
【iOS小组件】小组件尺寸及类型适配
swiftui·swift
Adam.com3 天前
#Swift :回调地狱 的解决 —— 通过 task/await 来替代 nested mutiple trailing closure 来进行 回调的解耦
开发语言·swift
Anakki3 天前
【Swift官方文档】7.Swift集合类型
运维·服务器·swift
KeithTsui4 天前
集合论(ZFC)之 联合公理(Axiom of Union)注解
开发语言·其他·算法·binder·swift
東三城4 天前
【ios】---swift开发从入门到放弃
ios·swift
文件夹__iOS7 天前
[SwiftUI 开发] @dynamicCallable 与 callAsFunction:将类型实例作为函数调用
ios·swiftui·swift