我們一起了解 Swift 調(diào)度器
本文轉(zhuǎn)載自微信公眾號「Swift社區(qū)」,作者前端小工 。轉(zhuǎn)載本文請聯(lián)系Swift社區(qū)公眾號。
前言
iOS 應(yīng)用開發(fā)中最常見的錯(cuò)誤之一是線程錯(cuò)誤,當(dāng)開發(fā)者試圖從一個(gè)閉包中更新用戶界面時(shí),會出現(xiàn)這種錯(cuò)誤。為了解決這個(gè)問題,我們可以使用 DispatchQueue.main 和 threads。
在本教程中,我們將學(xué)習(xí)什么是調(diào)度器,以及我們?nèi)绾卧趇OS應(yīng)用開發(fā)中使用它們來管理隊(duì)列和循環(huán)。之前對 Swift、Combine 框架和 iOS 開發(fā)的知識是必要的。
讓我們開始吧!
什么是調(diào)度器?
根據(jù)調(diào)度器的文檔[1],調(diào)度器是 "一個(gè)定義何時(shí)何地執(zhí)行一個(gè)閉包的協(xié)議"。從本質(zhì)上講,調(diào)度器為開發(fā)者提供了一種在特定安排下執(zhí)行代碼的方式,有助于在應(yīng)用程序中運(yùn)行隊(duì)列命令。
開發(fā)人員可以通過使用調(diào)度器將大批量的操作遷移到二級隊(duì)列中,釋放出應(yīng)用程序主隊(duì)列的空間,并更新應(yīng)用程序的用戶界面。
調(diào)度器還可以優(yōu)化并行執(zhí)行命令的代碼,允許開發(fā)者在同一時(shí)間執(zhí)行更多的命令。如果代碼是串行的,開發(fā)者可以一次執(zhí)行一個(gè)位的代碼。
調(diào)度器的類型
有幾種類型的調(diào)度器是Combine 內(nèi)置的[2]。值得注意的是,調(diào)度器遵循調(diào)度器協(xié)議,這可以在上面鏈接的調(diào)度器文檔中找到。
讓我們看一下幾個(gè)流行的調(diào)度器
OperationQueue
根據(jù)其文件,一個(gè) OperationQueue 會根據(jù)命令的優(yōu)先級和準(zhǔn)備程度來執(zhí)行命令。一旦你把一個(gè)操作添加到隊(duì)列中,該操作將保持在其隊(duì)列中,直到它完成執(zhí)行其命令。
一個(gè) OperationQueue,可以以串行或并行的方式執(zhí)行任務(wù),這取決于任務(wù)本身。OperationQueue 主要用于后臺任務(wù),如更新應(yīng)用程序的用戶界面。
DispatchQueue
蘋果公司的文檔將一個(gè) DispatchQueue[3]是一個(gè)先入先出的隊(duì)列,它可以接受塊對象形式的任務(wù),并以串行或并發(fā)的方式執(zhí)行它們。
系統(tǒng)會在一個(gè)線程池上管理提交給 DispatchQueue 的工作。除非 DispatchQueue 代表一個(gè)應(yīng)用程序的主線程,否則 DispatchQueue 并不保證它將使用哪個(gè)線程來執(zhí)行一個(gè)任務(wù)。
DispatchQueue 經(jīng)常被認(rèn)為是調(diào)度命令的最安全方式之一。然而,不建議在 Xcode 11[4] 中使用 DispatchQueue。如果你在 Xcode 11 中使用 DispatchQueue 作為調(diào)度器,它必須是串行的,以遵守 Combine 的操作符的契約。
ImmediateScheduler
一個(gè) ImmediateScheduler用來立即執(zhí)行異步操作。
- import Combine
- let immediateScheduler = ImmediateScheduler.shared
- let aNum = [1, 2, 3].publisher
- .receive(on: immediateScheduler)
- .sink(receiveValue: {
- print("Received \$0) on thread \(Threa.currentT")t
- })
例如,上面的代碼塊將發(fā)送一個(gè)類似于下面的代碼塊的輸出。
- Received 1 on thread <NSThread: 0x400005c480>{number = 1, name = main}
- Received 2 on thread <NSThread: 0x400005c480>{number = 1, name = main}
- Received 3 on thread <NSThread: 0x400005c480>{number = 1, name = main}
ImmediateScheduler 在應(yīng)用程序的當(dāng)前線程上立即執(zhí)行命令。上面的代碼塊是在主線程上運(yùn)行的。
RunLoop
RunLoop 調(diào)度器用于在一個(gè)特定的運(yùn)行循環(huán)上執(zhí)行任務(wù)。在運(yùn)行循環(huán)上的行動可能是不安全的,因?yàn)?RunLoops 不是線程安全的。因此,使用 DispatchQueue 是一個(gè)更好的選擇。
默認(rèn)的調(diào)度器
如果你沒有為一個(gè)任務(wù)指定調(diào)度器,Combine 會為它提供一個(gè)默認(rèn)的調(diào)度器。所提供的調(diào)度器將使用執(zhí)行該任務(wù)的同一線程。例如,如果你執(zhí)行一個(gè) UI 任務(wù),Combine 提供的調(diào)度器會在同一個(gè)UI線程上接收該任務(wù)。
切換調(diào)度器
在使用 Combine 的 iOS 開發(fā)中,許多消耗資源的任務(wù)都是在后臺完成的,以防止應(yīng)用程序的 UI 凍結(jié)或完全崩潰。然后,Combine 切換調(diào)度器,使任務(wù)的結(jié)果在主線程上執(zhí)行。
Combine使用兩種內(nèi)置方法來切換調(diào)度器:receive(on) 和 subscribe(on)。
receive(on)
receive(on) 方法用于在一個(gè)特定的調(diào)度器上發(fā)出數(shù)值。它為任何在它被聲明后的發(fā)布者改變一個(gè)調(diào)度器,如下面的代碼塊所示。
- Just(3)
- .map { _ in print(Thread.isMainThread) }
- .receive(on: DispatchQueue.global())
- .map { print(Thread.isMainThread) }
- .sink { print(Thread.isMainThread) }
上面的代碼塊將打印出以下結(jié)果。
- true
- false
- false
subscribe(on)
subscribe(on) 方法被用來在一個(gè)特定的調(diào)度器上創(chuàng)建一個(gè)訂閱。
- import Combine
- print("Current thread \(Thread.current)")
- let k = [a, b, c, d, e].publisher
- .subscribe(on: aQueue)
- .sick(receiveValue: {
- print(" got \($0) on thread \(Thread.current)")
- })
上面的代碼塊將打印出以下結(jié)果。
- Current thread <NSThread: 0x400005c480>{number = 1, name = main}
- Received a on thread <NSThread: 0x400005c480>{number = 7, name = null}
- Received b on thread <NSThread: 0x400005c480>{number = 7, name = null}
- Received c on thread <NSThread: 0x400005c480>{number = 7, name = null}
- Received d on thread <NSThread: 0x400005c480>{number = 7, name = null}
- Received e on thread <NSThread: 0x400005c480>{number = 7, name = null}
在上面的代碼塊中,這些值是從不同的線程而不是主線程發(fā)出的。subscribe(on) 方法串行地執(zhí)行任務(wù),從執(zhí)行指令的順序可以看出。
用調(diào)度器執(zhí)行異步任務(wù)
在本節(jié)中,我們將學(xué)習(xí)如何在 subscribe(on) 和 receive(on) 調(diào)度器方法之間進(jìn)行切換。想象一下,一個(gè)發(fā)布者正在后臺運(yùn)行一個(gè)任務(wù)。
- struct BackgroundPublisher: Publisher
- typealias Output = Int
- typealias Failure = Never
- func receive<K>(subscriber: K) where K : Subcriber, Failure == K.Failure, Output == K.Input {
- sleep(12)
- subscriber. receive(subscriptiton: Subscriptions.empty)
- _= subscriber.receive(3)
- subscriber.receive(completion: finished)
- }
如果我們從一個(gè)用戶界面線程中調(diào)用該任務(wù),我們的應(yīng)用程序?qū)鼋Y(jié) 12 秒。Combine 將在我們?nèi)蝿?wù)執(zhí)行的同一個(gè)調(diào)度器中添加一個(gè)默認(rèn)的調(diào)度器。
- BackgroundPublisher()
- .sink { _ in print("value received") }
- print("Hi!")
在上面的代碼塊中,Hi!,在接收到數(shù)值后,會在我們的控制臺中打印出來。我們可以看到下面的結(jié)果。
- value received
- Hi!
在 Combine 中,這種類型的異步工作經(jīng)常通過在后臺調(diào)度器上訂閱和在用戶界面調(diào)度器上接收事件來執(zhí)行。
- BackgroundPublisher()
- .subscribe(on: DispatchQueue.global())
- .receive(on: DispatchQueue.main)
- .sink { _ in print("Value recieved") }
- print("Hi Again!")
上面的代碼片斷將打印出下面的結(jié)果。
- Hi Again!
- Value received
Hi Again! ,在接收到數(shù)值之前被打印出來。現(xiàn)在,發(fā)布者不會因?yàn)樽枞覀兊闹骶€程而凍結(jié)我們的應(yīng)用程序。
總結(jié)
在這篇文章中,我們回顧了什么是調(diào)度器以及它們?nèi)绾卧?iOS 應(yīng)用程序中工作。我們介紹了一些最佳的使用案例,包括 OperationQueue, DispatchQueue, ImmediateScheduler, 和 RunLoop 。我們還談到了 Combine 框架以及它是如何影響 Swift 中調(diào)度器的使用。
我們學(xué)習(xí)了如何在 Swift 中使用 receive(on) 和 subscribe(on) 方法來切換調(diào)度器。我們還學(xué)習(xí)了如何在 Combine 中使用調(diào)度器執(zhí)行異步功能,即在后臺調(diào)度器上訂閱并在用戶界面調(diào)度器上接收我們的值。
譯自 Understanding Swift schedulers[5]
參考資料
[1]調(diào)度器: https://developer.apple.com/documentation/combine/scheduler
[2]Combine: https://developer.apple.com/documentation/combine
[3]DispatchQueue: https://developer.apple.com/documentation/dispatch/dispatchqueue#:~:text=Dispatch%20queues%20are%20FIFO%20queues,tasks%20either%20serially%20or%20concurrently.&text=When%20you%20schedule%20a%20work%20item%20asynchronously%2C%20your%20code%20continues,the%20work%20item%20runs%20elsewhere.
[4]Xcode 11: https://forums.swift.org/t/runloop-main-or-dispatchqueue-main-when-using-combine-scheduler/26635/4
[5]Understanding Swift schedulers: https://blog.logrocket.com/understanding-swift-schedulers/