Grand Central Dispatch(简称GCD)是大多数Swift开发人员无数次使用的基本技术之一。它主要用于能够在不同的并发队列上调度工作,并且经常用于编写如下代码:
DispatchQueue.main.async {
// Run async code on the main queue
}
复制代码
但事实证明,如果我们深入一点,GCD还有一套非常强大的API和功能,并不是每个人都知道的。本周,让我们先async {}
看看GCD可能非常有用的一些情况,以及它如何为许多其他 - 更常见的 - 基础API提供更简单(和更“Swifty”)的选项。
使用延迟延迟可取消的任务 DispatchWorkItem
关于GCD的一个常见误解是*“一旦你安排了一项无法取消的任务,就需要使用Operation
API”*。虽然过去曾经如此,但DispatchWorkItem
引入了iOS 8和macOS 10.10 ,它在一个非常易于使用的API中提供了这一功能。
假设我们的UI有一个搜索栏,当用户键入一个字符时,我们通过调用我们的后端来执行搜索。由于用户可以非常快速地打字,我们不想立即启动我们的网络请求(这可能会浪费大量数据和服务器容量),而是我们将*“去抖”*这些事件并仅执行请求一旦用户没有输入0.25秒。
这就是它的DispatchWorkItem
用武之地。通过将我们的请求代码封装在一个工作项中,只要它被一个新的替换,我们就可以很容易地取消它,如下所示:
class SearchViewController: UIViewController, UISearchBarDelegate {
// We keep track of the pending work item as a property
private var pendingRequestWorkItem: DispatchWorkItem?
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Cancel the currently pending item
pendingRequestWorkItem?.cancel()
// Wrap our request in a work item
let requestWorkItem = DispatchWorkItem { [weak self] in
self?.resultsLoader.loadResults(forQuery: searchText)
}
// Save the new work item and execute it after 250 ms
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
execute: requestWorkItem)
}
}
复制代码
正如我们上面所看到的,使用DispatchWorkItem
Swift实际上比使用a Timer
或者更简单更好Operation
,这要归功于尾随闭包语法以及GCD导入Swift的程度。我们不需要@objc
标记方法或#selector
- 它们都可以用闭包来完成。
使用分组和链接任务 DispatchGroup
有时我们需要执行一组操作才能继续使用逻辑。例如,假设我们需要在创建模型之前从一组数据源加载数据。我们不必自己跟踪所有数据源,而是可以轻松地将工作与工作同步 DispatchGroup
。
使用调度组还为我们提供了一个很大的优势,即我们的任务可以在不同的队列中同时运行。这使我们能够从简单开始,然后在需要时轻松添加并发,而无需重写任何任务。我们所要做的就是做出平衡的电话enter()
和leave()
一个调度组把它同步我们的任务。
让我们看一个例子,我们从本地存储,iCloud Drive和后端系统加载笔记,然后将所有结果合并到NoteCollection
:
// First, we create a group to synchronize our tasks
let group = DispatchGroup()
// NoteCollection is a thread-safe collection class for storing notes
let collection = NoteCollection()
// The 'enter' method increments the group's task count…
group.enter()
localDataSource.load { notes in
collection.add(notes)
// …while the 'leave' methods decrements it
group.leave()
}
group.enter()
iCloudDataSource.load { notes in
collection.add(notes)
group.leave()
}
group.enter()
backendDataSource.load { notes in
collection.add(notes)
group.leave()
}
// This closure will be called when the group's task count reaches 0
group.notify(queue: .main) { [weak self] in
self?.render(collection)
}
复制代码
上面的代码有效,但它有很多重复。让我们将其重构为扩展Array
,使用DataSource
协议作为其Element
类型的相同类型约束:
extension Array where Element == DataSource {
func load(completionHandler: @escaping (NoteCollection) -> Void) {
let group = DispatchGroup()
let collection = NoteCollection()
// De-duplicate the synchronization code by using a loop
for dataSource in self {
group.enter()
dataSource.load { notes in
collection.add(notes)
group.leave()
}
}
group.notify(queue: .main) {
completionHandler(collection)
}
}
}
复制代码
通过上面的扩展,我们现在可以将以前的代码减少到:
let dataSources: [DataSource] = [
localDataSource,
iCloudDataSource,
backendDataSource
]
dataSources.load { [weak self] collection in
self?.render(collection)
}
复制代码
非常好,紧凑!?
等待异步任务 DispatchSemaphore
虽然DispatchGroup
提供了一种同步一组异步操作同时仍保持异步的简单方法,但DispatchSemaphore
它提供了一种同步等待一组异步任务的方法。这在命令行工具或脚本中非常有用,我们没有应用程序运行循环,而只是在全局上下文中同步执行,直到完成为止。
就像DispatchGroup
,信号量API非常简单,因为我们只通过调用wait()
或递增或递减内部计数器signal()
。wait()
在a之前调用signal()
将*阻止当前队列,*直到收到信号。
让我们在Array
之前的扩展中创建另一个重载,它会NoteCollection
同步返回,否则会抛出错误。我们将重用DispatchGroup
以前的基于代码的代码,但只需使用信号量协调该任务。
extension Array where Element == DataSource {
func load() throws -> NoteCollection {
let semaphore = DispatchSemaphore(value: 0)
var loadedCollection: NoteCollection?
// We create a new queue to do our work on, since calling wait() on
// the semaphore will cause it to block the current queue
let loadingQueue = DispatchQueue.global()
loadingQueue.async {
// We extend 'load' to perform its work on a specific queue
self.load(onQueue: loadingQueue) { collection in
loadedCollection = collection
// Once we're done, we signal the semaphore to unblock its queue
semaphore.signal()
}
}
// Wait with a timeout of 5 seconds
semaphore.wait(timeout: .now() + 5)
guard let collection = loadedCollection else {
throw NoteLoadingError.timedOut
}
return collection
}
}
复制代码
使用上面的新方法Array
,我们现在可以在脚本或命令行工具中同步加载注释,如下所示:
let dataSources: [DataSource] = [
localDataSource,
iCloudDataSource,
backendDataSource
]
do {
let collection = try dataSources.load()
output(collection)
} catch {
output(error)
}
复制代码
观察文件中的更改 DispatchSource
我想提出的最后一个*“鲜为人知”的GCD功能是它如何提供一种方法来观察文件系统上文件的变化。比如DispatchSemaphore
,如果我们想要自动对用户正在编辑的文件做出反应,这在脚本或命令行工具中非常有用。这使我们能够轻松构建具有“实时编辑”*功能的开发人员工具。
根据我们想要观察的内容,调度源有几种不同的变体。在这种情况下,我们将使用DispatchSourceFileSystemObject
,它允许我们观察文件系统中的事件。
让我们看一个简单的示例实现FileObserver
,它允许我们在每次更改给定文件时附加一个闭包。它通过使用a fileDescriptor
和a 创建调度源DispatchQueue
来执行观察,并使用Files来引用要观察的文件:
class FileObserver {
private let file: File
private let queue: DispatchQueue
private var source: DispatchSourceFileSystemObject?
init(file: File) {
self.file = file
self.queue = DispatchQueue(label: "com.myapp.fileObserving")
}
func start(closure: @escaping () -> Void) {
// We can only convert an NSString into a file system representation
let path = (file.path as NSString)
let fileSystemRepresentation = path.fileSystemRepresentation
// Obtain a descriptor from the file system
let fileDescriptor = open(fileSystemRepresentation, O_EVTONLY)
// Create our dispatch source
let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileDescriptor,
eventMask: .write,
queue: queue
)
// Assign the closure to it, and resume it to start observing
source.setEventHandler(handler: closure)
source.resume()
self.source = source
}
}
复制代码
我们现在可以FileObserver
像这样使用:
let observer = try FileObserver(file: file)
observer.start {
print("File was changed")
}
复制代码
想象一下所有可以使用它构建的很酷的开发人员工具!?
结论
Grand Central Dispatch是一个非常强大的框架,它的功能远远超出它的初期状态。希望这篇文章能够激发你对它的用处的想象力,我建议你在下次需要完成我们在这篇文章中看到的任务之一时尝试一下。
在我看来,许多基于Timer或者OperationQueue的代码,以及第三方异步框架的使用,实际上可以通过直接使用GCD变得更简单。