本文转载自微信公众号「Swift社区」,作者前端小工 。转载本文请联系Swift社区公众号。
前言
iOS 应用开发中最常见的错误之一是线程错误,当开发者试图从一个闭包中更新用户界面时,会出现这种错误。为了解决这个问题,我们可以使用 DispatchQueue.main 和 threads。
在本教程中,我们将学习什么是调度器,以及我们如何在iOS应用开发中使用它们来管理队列和循环。之前对 Swift、Combine 框架和 iOS 开发的知识是必要的。
让我们开始吧!
什么是调度器?
根据调度器的文档[1],调度器是 "一个定义何时何地执行一个闭包的协议"。从本质上讲,调度器为开发者提供了一种在特定安排下执行代码的方式,有助于在应用程序中运行队列命令。
开发人员可以通过使用调度器将大批量的操作迁移到二级队列中,释放出应用程序主队列的空间,并更新应用程序的用户界面。
调度器还可以优化并行执行命令的代码,允许开发者在同一时间执行更多的命令。如果代码是串行的,开发者可以一次执行一个位的代码。
调度器的类型
有几种类型的调度器是Combine 内置的[2]。值得注意的是,调度器遵循调度器协议,这可以在上面链接的调度器文档中找到。
让我们看一下几个流行的调度器
OperationQueue
根据其文件,一个 OperationQueue 会根据命令的优先级和准备程度来执行命令。一旦你把一个操作添加到队列中,该操作将保持在其队列中,直到它完成执行其命令。
一个 OperationQueue,可以以串行或并行的方式执行任务,这取决于任务本身。OperationQueue 主要用于后台任务,如更新应用程序的用户界面。
DispatchQueue
苹果公司的文档将一个 DispatchQueue[3]是一个先入先出的队列,它可以接受块对象形式的任务,并以串行或并发的方式执行它们。
系统会在一个线程池上管理提交给 DispatchQueue 的工作。除非 DispatchQueue 代表一个应用程序的主线程,否则 DispatchQueue 并不保证它将使用哪个线程来执行一个任务。
DispatchQueue 经常被认为是调度命令的最安全方式之一。然而,不建议在 Xcode 11[4] 中使用 DispatchQueue。如果你在 Xcode 11 中使用 DispatchQueue 作为调度器,它必须是串行的,以遵守 Combine 的操作符的契约。
ImmediateScheduler
一个 ImmediateScheduler用来立即执行异步操作。
- 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
- })
例如,上面的代码块将发送一个类似于下面的代码块的输出。
- 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 在应用程序的当前线程上立即执行命令。上面的代码块是在主线程上运行的。
RunLoop
RunLoop 调度器用于在一个特定的运行循环上执行任务。在运行循环上的行动可能是不安全的,因为 RunLoops 不是线程安全的。因此,使用 DispatchQueue 是一个更好的选择。
默认的调度器
如果你没有为一个任务指定调度器,Combine 会为它提供一个默认的调度器。所提供的调度器将使用执行该任务的同一线程。例如,如果你执行一个 UI 任务,Combine 提供的调度器会在同一个UI线程上接收该任务。
切换调度器
在使用 Combine 的 iOS 开发中,许多消耗资源的任务都是在后台完成的,以防止应用程序的 UI 冻结或完全崩溃。然后,Combine 切换调度器,使任务的结果在主线程上执行。
Combine使用两种内置方法来切换调度器:receive(on) 和 subscribe(on)。
receive(on)
receive(on) 方法用于在一个特定的调度器上发出数值。它为任何在它被声明后的发布者改变一个调度器,如下面的代码块所示。
- Just(3)
- .map { _ in print(Thread.isMainThread) }
- .receive(on: DispatchQueue.global())
- .map { print(Thread.isMainThread) }
- .sink { print(Thread.isMainThread) }
上面的代码块将打印出以下结果。
- true
- false
- false
subscribe(on)
subscribe(on) 方法被用来在一个特定的调度器上创建一个订阅。
- 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)")
- })
上面的代码块将打印出以下结果。
- 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}
在上面的代码块中,这些值是从不同的线程而不是主线程发出的。subscribe(on) 方法串行地执行任务,从执行指令的顺序可以看出。
用调度器执行异步任务
在本节中,我们将学习如何在 subscribe(on) 和 receive(on) 调度器方法之间进行切换。想象一下,一个发布者正在后台运行一个任务。
- 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)
- }
如果我们从一个用户界面线程中调用该任务,我们的应用程序将冻结 12 秒。Combine 将在我们任务执行的同一个调度器中添加一个默认的调度器。
- BackgroundPublisher()
- .sink { _ in print("value received") }
- print("Hi!")
在上面的代码块中,Hi!,在接收到数值后,会在我们的控制台中打印出来。我们可以看到下面的结果。
- value received
- Hi!
在 Combine 中,这种类型的异步工作经常通过在后台调度器上订阅和在用户界面调度器上接收事件来执行。
- BackgroundPublisher()
- .subscribe(on: DispatchQueue.global())
- .receive(on: DispatchQueue.main)
- .sink { _ in print("Value recieved") }
- print("Hi Again!")
上面的代码片断将打印出下面的结果。
- Hi Again!
- Value received
Hi Again! ,在接收到数值之前被打印出来。现在,发布者不会因为阻塞我们的主线程而冻结我们的应用程序。
总结
在这篇文章中,我们回顾了什么是调度器以及它们如何在 iOS 应用程序中工作。我们介绍了一些最佳的使用案例,包括 OperationQueue, DispatchQueue, ImmediateScheduler, 和 RunLoop 。我们还谈到了 Combine 框架以及它是如何影响 Swift 中调度器的使用。
我们学习了如何在 Swift 中使用 receive(on) 和 subscribe(on) 方法来切换调度器。我们还学习了如何在 Combine 中使用调度器执行异步功能,即在后台调度器上订阅并在用户界面调度器上接收我们的值。
译自 Understanding Swift schedulers[5]
参考资料
[1]调度器: 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/