Async/await語法是在Swift 5.5 引入的赔桌,在 WWDC 2021中的 Meet async/await in Swift 對齊進行了介紹在辆。它是編寫異步代碼的一種更可讀的方式工闺,比調(diào)度隊列和回調(diào)函數(shù)更容易理解。Async/await 語法與其他編程語言(如C#或JavaScript)中使用的語法類似温峭。使用 "async let "是為了并行的運行多個后臺任務(wù)猛铅,并等待它們的綜合結(jié)果。
Swift異步編程是一種編寫允許某些任務(wù)并發(fā)運行而不是按順序運行的代碼的方法凤藏。這可以提高應(yīng)用程序的性能奸忽,允許它同時執(zhí)行多個任務(wù)堕伪,但更重要的是,它可以用來確保用戶界面對用戶輸入的響應(yīng)栗菜,同時任務(wù)在后臺線程上執(zhí)行欠雌。
長期運行的任務(wù)阻塞了UI
在一個同步的程序中,代碼以線性的疙筹、從上到下的方式運行富俄。程序等待當前任務(wù)完成后再進入下一任務(wù)。這在用戶界面(UI)方面會產(chǎn)生問題而咆,因為如果一個長期運行的任務(wù)被同步執(zhí)行霍比,程序就會阻塞,UI就會變得沒有反應(yīng)暴备,直到任務(wù)完成悠瞬。
下面的代碼模擬了一個長期運行的任務(wù),如以同步方式下載一個文件涯捻,其結(jié)果是UI 變得沒有反應(yīng)浅妆,直到任務(wù)完成。這樣的用戶體驗是不可接受的汰瘫。
Model:
struct DataFile : Identifiable, Equatable {
var id: Int
var fileSize: Int
var downloadedSize = 0
var isDownloading = false
init(id: Int, fileSize: Int) {
self.id = id
self.fileSize = fileSize
}
var progress: Double {
return Double(self.downloadedSize) / Double(self.fileSize)
}
mutating func increment() {
if downloadedSize < fileSize {
downloadedSize += 1
}
}
}
ViewModel:
class DataFileViewModel: ObservableObject {
@Published private(set) var file: DataFile
init() {
self.file = DataFile(id: 1, fileSize: 10)
}
func downloadFile() {
file.isDownloading = true
for _ in 0..<file.fileSize {
file.increment()
usleep(300000)
}
file.isDownloading = false
}
func reset() {
self.file = DataFile(id: 1, fileSize: 10)
}
}
View:
struct TestView1: View {
@ObservedObject private var dataFiles: DataFileViewModel
init() {
dataFiles = DataFileViewModel()
}
var body: some View {
VStack {
/// 從文末源代碼獲取其實現(xiàn)
TitleView(title: ["Synchronous"])
Button("Download All") {
dataFiles.downloadFile()
}
.buttonStyle(BlueButtonStyle())
.disabled(dataFiles.file.isDownloading)
HStack(spacing: 10) {
Text("File 1:")
ProgressView(value: dataFiles.file.progress)
.frame(width: 180)
Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")
ZStack {
Color.clear
.frame(width: 30, height: 30)
if dataFiles.file.isDownloading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
}
}
}
.padding()
Spacer().frame(height: 200)
Button("Reset") {
dataFiles.reset()
}
.buttonStyle(BlueButtonStyle())
Spacer()
}
.padding()
}
}
使用 async/await 在后臺執(zhí)行任務(wù)
將 ViewModel 中的downloadFile
方法修改為異步的狂打。請注意,由于DataFile
模型是被視圖監(jiān)聽的混弥,對模型的任何改變都需要在UI線程上執(zhí)行。這是通過使用 MainActor 隊列來完成的对省,即用MainActor.run
包裹所有的模型更新蝗拿。
ViewModel
class DataFileViewModel2: ObservableObject {
@Published private(set) var file: DataFile
init() {
self.file = DataFile(id: 1, fileSize: 10)
}
func downloadFile() async -> Int {
await MainActor.run {
file.isDownloading = true
}
for _ in 0..<file.fileSize {
await MainActor.run {
file.increment()
}
usleep(300000)
}
await MainActor.run {
file.isDownloading = false
}
return 1
}
func reset() {
self.file = DataFile(id: 1, fileSize: 10)
}
}
View:
struct TestView2: View {
@ObservedObject private var dataFiles: DataFileViewModel2
@State var fileCount = 0
init() {
dataFiles = DataFileViewModel2()
}
var body: some View {
VStack {
TitleView(title: ["Asynchronous"])
Button("Download All") {
Task {
let num = await dataFiles.downloadFile()
fileCount += num
}
}
.buttonStyle(BlueButtonStyle())
.disabled(dataFiles.file.isDownloading)
Text("Files Downloaded: \(fileCount)")
HStack(spacing: 10) {
Text("File 1:")
ProgressView(value: dataFiles.file.progress)
.frame(width: 180)
Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")
ZStack {
Color.clear
.frame(width: 30, height: 30)
if dataFiles.file.isDownloading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
}
}
}
.padding()
Spacer().frame(height: 200)
Button("Reset") {
dataFiles.reset()
}
.buttonStyle(BlueButtonStyle())
Spacer()
}
.padding()
}
}
在后臺執(zhí)行多個任務(wù)
現(xiàn)在我們有一個文件在后臺下載蒿涎,UI顯示進度哀托,讓我們把它改為多個文件。ViewModel
被改為持有一個DataFiles
數(shù)組劳秋,而不是一個單一的文件仓手。添加一個downloadFiles
方法來遍歷所有文件并下載每一個。
視圖被綁定到DataFiles
數(shù)組玻淑,并更新顯示每個文件的下載進度嗽冒。下載按鈕被綁定到異步的downloadFiles
中。
ViewModel:
class DataFileViewModel3: ObservableObject {
@Published private(set) var files: [DataFile]
@Published private(set) var fileCount = 0
init() {
files = [
DataFile(id: 1, fileSize: 10),
DataFile(id: 2, fileSize: 20),
DataFile(id: 3, fileSize: 5)
]
}
var isDownloading : Bool {
files.filter { $0.isDownloading }.count > 0
}
func downloadFiles() async {
for index in files.indices {
let num = await downloadFile(index)
await MainActor.run {
fileCount += num
}
}
}
private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
await MainActor.run {
files[index].isDownloading = true
}
for _ in 0..<files[index].fileSize {
await MainActor.run {
files[index].increment()
}
usleep(300000)
}
await MainActor.run {
files[index].isDownloading = false
}
return 1
}
func reset() {
files = [
DataFile(id: 1, fileSize: 10),
DataFile(id: 2, fileSize: 20),
DataFile(id: 3, fileSize: 5)
]
}
}
View:
struct TestView3: View {
@ObservedObject private var dataFiles: DataFileViewModel3
init() {
dataFiles = DataFileViewModel3()
}
var body: some View {
VStack {
TitleView(title: ["Asynchronous", "(multiple Files)"])
Button("Download All") {
Task {
await dataFiles.downloadFiles()
}
}
.buttonStyle(BlueButtonStyle())
.disabled(dataFiles.isDownloading)
Text("Files Downloaded: \(dataFiles.fileCount)")
ForEach(dataFiles.files) { file in
HStack(spacing: 10) {
Text("File \(file.id):")
ProgressView(value: file.progress)
.frame(width: 180)
Text("\((file.progress * 100), specifier: "%0.0F")%")
ZStack {
Color.clear
.frame(width: 30, height: 30)
if file.isDownloading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
}
}
}
}
.padding()
Spacer().frame(height: 150)
Button("Reset") {
dataFiles.reset()
}
.buttonStyle(BlueButtonStyle())
Spacer()
}
.padding()
}
}
使用 "async let "來模擬并發(fā)下載多個文件的情況
上面的代碼可以被改進补履,以并行地執(zhí)行多個下載添坊,因為每個任務(wù)都是獨立于其他任務(wù)的。在Swift并發(fā)中箫锤,這是用async let
實現(xiàn)的贬蛙,它用一個承諾立即給一個變量賦值雨女,允許代碼執(zhí)行下一行代碼。然后阳准,代碼等待這些承諾氛堕,等待最終結(jié)果的完成。
async/await:
func downloadFiles() async {
for index in files.indices {
let num = await downloadFile(index)
await MainActor.run {
fileCount += num
}
}
}
async let
func downloadFiles() async {
async let num1 = await downloadFile(0)
async let num2 = await downloadFile(1)
async let num3 = await downloadFile(2)
let (result1, result2, result3) = await (num1, num2, num3)
await MainActor.run {
fileCount = result1 + result2 + result3
}
}
ViewModel
class DataFileViewModel4: ObservableObject {
@Published private(set) var files: [DataFile]
@Published private(set) var fileCount = 0
init() {
files = [
DataFile(id: 1, fileSize: 10),
DataFile(id: 2, fileSize: 20),
DataFile(id: 3, fileSize: 5)
]
}
var isDownloading : Bool {
files.filter { $0.isDownloading }.count > 0
}
func downloadFiles() async {
async let num1 = await downloadFile(0)
async let num2 = await downloadFile(1)
async let num3 = await downloadFile(2)
let (result1, result2, result3) = await (num1, num2, num3)
await MainActor.run {
fileCount = result1 + result2 + result3
}
}
private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
await MainActor.run {
files[index].isDownloading = true
}
for _ in 0..<files[index].fileSize {
await MainActor.run {
files[index].increment()
}
usleep(300000)
}
await MainActor.run {
files[index].isDownloading = false
}
return 1
}
func reset() {
files = [
DataFile(id: 1, fileSize: 10),
DataFile(id: 2, fileSize: 20),
DataFile(id: 3, fileSize: 5)
]
}
}
View
struct TestView4: View {
@ObservedObject private var dataFiles: DataFileViewModel4
init() {
dataFiles = DataFileViewModel4()
}
var body: some View {
VStack {
TitleView(title: ["Parallel", "(multiple Files)"])
Button("Download All") {
Task {
await dataFiles.downloadFiles()
}
}
.buttonStyle(BlueButtonStyle())
.disabled(dataFiles.isDownloading)
Text("Files Downloaded: \(dataFiles.fileCount)")
ForEach(dataFiles.files) { file in
HStack(spacing: 10) {
Text("File \(file.id):")
ProgressView(value: file.progress)
.frame(width: 180)
Text("\((file.progress * 100), specifier: "%0.0F")%")
ZStack {
Color.clear
.frame(width: 30, height: 30)
if file.isDownloading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
}
}
}
}
.padding()
Spacer().frame(height: 150)
Button("Reset") {
dataFiles.reset()
}
.buttonStyle(BlueButtonStyle())
Spacer()
}
.padding()
}
}
結(jié)論
在后臺執(zhí)行長期運行的任務(wù)并保持UI的響應(yīng)是很重要的野蝇。async/await提供了一個干凈的機制來執(zhí)行異步任務(wù)岔擂。有的時候,一個方法在后臺調(diào)用多個方法浪耘,默認情況下是按順序進行這些調(diào)用乱灵。async 讓其立即返回,允許代碼進行下一個調(diào)用七冲,然后所有返回的對象可以一起等待痛倚。這使得多個后臺任務(wù)可以并行進行。
GitHub 上提供了 AsyncLetApp 的源代碼澜躺。
譯自 https://swdevnotes.com/swift/2023/use-async-let-to-run-background-tasks-in-parallel-in-swift/