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。

相关推荐
Swift社区1 天前
LeetCode - #182 Swift 实现找出重复的电子邮件
算法·leetcode·swift
货拉拉技术1 天前
货拉拉用户端SwiftUI踩坑之旅
ios·swiftui·swift
来自于狂人3 天前
Openstack持久存储-Swift,Cinder,Manila三者之间的区别
服务器·openstack·swift
ZacJi4 天前
巧用 allowsHitTesting 自定义 SignInWithAppleButton
ios·swiftui·swift
刘争Stanley6 天前
SwiftUI 是如何改变 iOS 开发游戏规则的?
ios·swiftui·swift
Swift社区7 天前
LeetCode - #180 Swift 实现连续数字查询
算法·leetcode·swift
请叫我飞哥@8 天前
HTML5 缩放动画(Zoom In/Out)详解
前端·html5·swift
打工人你好8 天前
Swift Protocols(协议)、Extensions(扩展)、Error Handling(错误处理)、Generics(泛型)
swift
JR_Wang24918 天前
【iOS Swift Moya 最新请求网络框架封装通用】
网络·ios·swift
TripleEyeAlien10 天前
Swift Combine 学习(三):Subscription和 Subscriber
学习·ios·swift