Swift 中的 MainActor 使用和主線程調度

MainActor 是Swift 5.5中引入的一個新屬性,它是一個全局 actor,提供一個在主線程上執行任務的執行器。在構建應用程序時,在主線程上執行UI更新任務是很重要的,在使用幾個後臺線程時,這有時會很有挑戰性。使用@MainActor屬性將幫助你確保你的UI總是在主線程上更新。

如果您不熟悉 Swift 中的 Actors,我建議您閱讀我的文章Swift中的Actors 使用以如何及防止數據競爭,全局Actors的行爲類似於Actors,我不會在這篇文章中詳細介紹Actors的工作方式。

什麼是 MainActor?

MainActor 是一個全局唯一的 Actor,他在主線程上執行他的任務。它應該被用於屬性、方法、實例和閉包,以在主線程上執行任務。提案SE-0316 全局Actor 引入了 MainActor,作爲其全局 Actor 的一個例子,它繼承了GlobalActor協議。

理解全局 Actors

全局 Actor 可以看作是單例:每個只有一個實例。如果你的Xcode不支持,請升級到最新版本或者通過啓用實驗併發來工作。您可以通過在 Xcode 的構建設置中將以下值添加到“Other Swift Flags”中來實現:

-Xfrontend -enable-experimental-concurrency

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

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

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

@SwiftLeeActor
final class SwiftLeeFetcher {
    // ..
}

如何在 Swift 中使用 MainActor

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

@MainActor
final class HomeViewModel {
    // ..
}

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

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

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

}

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

編譯器執行MainActor的屬性要求,可使用如下代碼修復錯誤:

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 = []
    }
}

單獨的方法也可以用該屬性進行標記:

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

甚至可以將閉包標記爲在主線程上執行:

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

直接使用 MainActor

Swift 中的 MainActor 帶有一個可以直接使用 Actor 的擴展:

@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了。

我應該在什麼時候使用MainActor屬性?

在 Swift 5.5 之前,你可能定義了很多調度語句,以確保任務在主線程上運行。一個例子可能是這樣的:

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)
        }
    }
} 

在上面的例子中,我們很確定需要一個調度。然而,在其他情況下,調度可能是不必要的,因爲我們已經在主線程上。這樣做會導致額外的調度被跳過。

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

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)
        }
    }
}

由於我們現在使用的是一個actor定義的閉包,我們需要使用 async-await 技術來調用我們的閉包。在這裏使用@MainActor屬性可以讓Swift編譯器對我們的代碼進行性能優化。

選擇正確的策略

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

在這些情況下,讓實現者負責調度到正確的隊列可能會更好。

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

繼續你的Swift併發之旅

併發的變化不僅僅是 async-await,還包括許多新的功能,你可以從你的代碼中受益。所以,當你在做這件事的時候,爲什麼不深入研究一下其他的併發功能呢?

結論

全局Actor是對Swift中的Actor的一個很好的補充。它允許我們重用常見的Actor,並使UI任務的執行成爲可能,因爲編譯器可以在內部優化我們的代碼。全局Actor可以用在屬性、方法、實例和閉包上,之後編譯器會確保要求在我們的代碼中得到保證。

轉自 MainActor usage in Swift explained to dispatch to the main thread

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章