Swift 中的 MainActor 使用和主線程調(diào)度

MainActor 是Swift 5.5中引入的一個(gè)新屬性,它是一個(gè)全局 actor,提供一個(gè)在主線程上執(zhí)行任務(wù)的執(zhí)行器。在構(gòu)建應(yīng)用程序時(shí),在主線程上執(zhí)行UI更新任務(wù)是很重要的,在使用幾個(gè)后臺(tái)線程時(shí),這有時(shí)會(huì)很有挑戰(zhàn)性。使用@MainActor屬性將幫助你確保你的UI總是在主線程上更新。

如果您不熟悉 Swift 中的 Actors,我建議您閱讀我的文章Swift中的Actors 使用以如何及防止數(shù)據(jù)競爭,全局Actors的行為類似于Actors,我不會(huì)在這篇文章中詳細(xì)介紹Actors的工作方式。

什么是 MainActor?

MainActor 是一個(gè)全局唯一的 Actor,他在主線程上執(zhí)行他的任務(wù)。它應(yīng)該被用于屬性、方法、實(shí)例和閉包,以在主線程上執(zhí)行任務(wù)。提案SE-0316 全局Actor 引入了 MainActor,作為其全局 Actor 的一個(gè)例子,它繼承了GlobalActor協(xié)議。

理解全局 Actors

全局 Actor 可以看作是單例:每個(gè)只有一個(gè)實(shí)例。如果你的Xcode不支持,請升級到最新版本或者通過啟用實(shí)驗(yàn)并發(fā)來工作。您可以通過在 Xcode 的構(gòu)建設(shè)置中將以下值添加到“Other Swift Flags”中來實(shí)現(xiàn):

-Xfrontend -enable-experimental-concurrency

我們可以定義我們自己的全局 Actor 如下:

@globalActor
actor SwiftLeeActor {
    static let shared = SwiftLeeActor()
}

共享屬性是GlobalActor協(xié)議的一個(gè)要求,它可以確保有一個(gè)全球唯一的角色實(shí)例。一旦被定義,你就可以在整個(gè)項(xiàng)目中使用全局Actor,就像你對其他 Actor 一樣:

@SwiftLeeActor
final class SwiftLeeFetcher {
    // ..
}

如何在 Swift 中使用 MainActor

全局actor可以與屬性、方法、閉包和實(shí)例一起使用。例如,我們可以將 MainActor屬性添加到視圖模型中,以使其在主線程上執(zhí)行所有任務(wù):

@MainActor
final class HomeViewModel {
    // ..
}

使用nonisolated,我們可以確保沒有主線程要求的方法盡可能快地執(zhí)行。如果一個(gè)類沒有父類,父類使用相同的全局actor注釋,或者父類是NSObject,則只能使用全局actor進(jìn)行注釋。 全局 Actor 注釋的類的子類必須與同一個(gè)全局 Actor 隔離。

在其他情況下,我們可能希望使用全局Actor定義單個(gè)屬性:

final class HomeViewModel {
    
    @MainActor var images: [UIImage] = []

}

@MainActor屬性標(biāo)記images屬性,可以確保它只能從主線程更新:

The MainActor attribute requirements are enforced by the compiler.

編譯器執(zhí)行MainActor的屬性要求,可使用如下代碼修復(fù)錯(cuò)誤:

final class HomeViewModel {
    @MainActor var images: [UIImage] = []
    func updateImages() async {
        await MainActor.run {
            images = []
        }
    }
}
// OR
final class HomeViewModel {
    @MainActor var images: [UIImage] = []
    @MainActor
    func updateImages() {
        images = []
    }
}

單獨(dú)的方法也可以用該屬性進(jìn)行標(biāo)記:

@MainActor func updateViews() {
    // Perform UI updates..
}

甚至可以將閉包標(biāo)記為在主線程上執(zhí)行:

func updateData(completion: @MainActor @escaping () -> ()) {
    /// Example dispatch to mimic behaviour
    DispatchQueue.global().async {
        async {
            await completion()
        }
    }
}

直接使用 MainActor

Swift 中的 MainActor 帶有一個(gè)可以直接使用 Actor 的擴(kuò)展:

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension MainActor {

    /// Execute the given body closure on the main actor.
    public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}

這允許我們直接在方法中使用 MainActor,即使我們沒有使用全局 actor 屬性定義它的任何主體:

async {
    await MainActor.run {
        // Perform UI updates
    }
}

換句話說,不再需要使用 DispatchQueue.main.async了。

我應(yīng)該在什么時(shí)候使用MainActor屬性?

在 Swift 5.5 之前,你可能定義了很多調(diào)度語句,以確保任務(wù)在主線程上運(yùn)行。一個(gè)例子可能是這樣的:

func fetchData(completion: @escaping (Result<[UIImage], Error>) -> Void) {
    URLSession.shared.dataTask(with: URL(string: "..some URL")) { data, response, error in
        // .. Decode data to a result
        
        DispatchQueue.main.async {
            completion(result)
        }
    }
} 

在上面的例子中,我們很確定需要一個(gè)調(diào)度。然而,在其他情況下,調(diào)度可能是不必要的,因?yàn)槲覀円呀?jīng)在主線程上。這樣做會(huì)導(dǎo)致額外的調(diào)度被跳過。

無論哪種方式,在這些情況下,將屬性、方法、實(shí)例或閉包定義為一個(gè)主行為體是有意義的,以確保任務(wù)在主線程上執(zhí)行。例如,我們可以把上面的例子改寫成如下:

func fetchData(completion: @MainActor @escaping (Result<[UIImage], Error>) -> Void) {
    URLSession.shared.dataTask(with: URL(string: "..some URL")!) { data, response, error in
        // .. Decode data to a result
        let result: Result<[UIImage], Error> = .success([])
        
        async {
            await completion(result)
        }
    }
}

由于我們現(xiàn)在使用的是一個(gè)actor定義的閉包,我們需要使用 async-await 技術(shù)來調(diào)用我們的閉包。在這里使用@MainActor屬性可以讓Swift編譯器對我們的代碼進(jìn)行性能優(yōu)化。

選擇正確的策略

使用 actors 時(shí)選擇正確的策略很重要。在上面的例子中,我們決定讓閉包成為一個(gè)actor,這意味著無論誰使用我們的方法,完成回調(diào)都將使用 MainActor 執(zhí)行。在某些情況下,如果數(shù)據(jù)請求方法也是從一個(gè)不需要在主線程上處理完成回調(diào)的地方使用,這可能就沒有意義了。

在這些情況下,讓實(shí)現(xiàn)者負(fù)責(zé)調(diào)度到正確的隊(duì)列可能會(huì)更好。

viewModel.fetchData { result in
    async {
        await MainActor.run {
            // Handle result
        }
    }
}

繼續(xù)你的Swift并發(fā)之旅

并發(fā)的變化不僅僅是 async-await,還包括許多新的功能,你可以從你的代碼中受益。所以,當(dāng)你在做這件事的時(shí)候,為什么不深入研究一下其他的并發(fā)功能呢?

結(jié)論

全局Actor是對Swift中的Actor的一個(gè)很好的補(bǔ)充。它允許我們重用常見的Actor,并使UI任務(wù)的執(zhí)行成為可能,因?yàn)榫幾g器可以在內(nèi)部優(yōu)化我們的代碼。全局Actor可以用在屬性、方法、實(shí)例和閉包上,之后編譯器會(huì)確保要求在我們的代碼中得到保證。

轉(zhuǎn)自 MainActor usage in Swift explained to dispatch to the main thread

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容