原文: https://www.raywenderlich.com/148515/grand-central-dispatch-tutorial-swift-3-part-2
欢迎来到Grand Central Dispatch教程系列的第二部分也是最后一部分!
在本系列的第一部分中,您了解了并发性,线程以及GCD的工作原理。您使用调度屏障和同步调度队列的组合来使单线程安全地读写。您还通过使用调度队列来延迟显示提示并在实例化视图控制器时异步卸载CPU密集型工作,从而增强了应用程序的用户体验。
在第二个Grand Central Dispatch教程中,您将使用第一部分中您熟悉并喜爱的相同GooglyPuff应用程序。您将深入研究高级GCD概念,包括调度组,取消调度块,异步测试技术和调度源。
现在是探索更多GCD的时候了!
入门
如果你遵循,你可以从第一部分的示例项目中选择你离开的地方。或者,您也可以从本教程的第一部分下载完成的项目。
运行应用程序,点击+,然后选择Le Internet以添加Internet照片。您可能会注意到下载完成警报消息在图像下载完成之前弹出:
这是你要解决的第一件事。
派遣小组
打开PhotoManager.swift并查看downloadPhotosWithCompletion(_:)
:
func downloadPhotosWithCompletion(
_ completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError?
for address in [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString] {
let url = URL(string: address)
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
}
PhotoManager.sharedManager.addPhoto(photo)
}
completion?(storedError)
}
该警报由completion
传入该方法的闭包激发。这是在for
下载照片的循环之后调用的。您错误地认为在调用闭包之前下载已完成。
照片下载通过呼叫启动DownloadPhoto(url:)
。此调用立即返回,但实际下载是异步发生的。因此,当completion
被调用时,不能保证所有下载都完成。
你想要的是在downloadPhotosWithCompletion(_:)
完成所有照片下载任务后调用它的完成关闭。如何监控这些并发异步事件以实现这一目标?使用当前的方法,您不知道任务何时完成,并且可以按任何顺序完成。
好消息!这正是调度组设计要处理的。使用派遣组可以将多个任务分组在一起,并等待他们完成或在完成后通知他们。任务可以是异步或同步的,甚至可以在不同的队列上运行。
DispatchGroup
管理派遣组。你会先看看它的wait
方法。这会阻止当前线程,直到所有组的入队任务完成。
在PhotoManager.swiftdownloadPhotosWithCompletion(_:)
中用下面的代码替换代码:
DispatchQueue.global(qos: .userInitiated).async { // 1
var storedError: NSError?
let downloadGroup = DispatchGroup() // 2
for address in [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString] {
let url = URL(string: address)
downloadGroup.enter() // 3
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
downloadGroup.leave() // 4
}
PhotoManager.sharedManager.addPhoto(photo)
}
downloadGroup.wait() // 5
DispatchQueue.main.async { // 6
completion?(storedError)
}
}
以下是代码正在逐步完成的内容:
- 由于您使用
wait
阻止当前线程的同步方法,因此您可以使用async
将整个方法放入后台队列中,以确保不会阻塞主线程。 - 这会创建一个新的调度组。
- 您打电话
enter()
来手动通知该组任务已启动。您必须平衡enter()
通话次数与leave()
通话次数,否则您的应用将崩溃。 - 在这里你通知小组这项工作已经完成。
- 您
wait()
在等待任务完成时调用以阻止当前线程。这永远等待,因为照片创建任务总是完成。您可以使用wait(timeout:)
指定超时并在指定时间后等待。 - 此时,您可以保证所有图像任务已完成或超时。然后,您打回主队列以运行完成关闭。
构建并运行应用程序。通过Le Internet选项下载照片并确认在下载所有图像之前警报不会显示。
盛大中央派遣教程注意:如果网络活动发生得太快以至于无法识别何时应该调用完成关闭并且您正在设备上运行应用程序,则可以通过在“设置”应用程序的“ 开发人员”部分中切换某些网络设置,确保这确实起作用。只需转到网络链接调节器部分,启用它,然后选择一个配置文件。“非常糟糕的网络”是一个不错的选择。
如果您正在模拟器上运行,则可以使用Xcode Advanced Tools中包含的Network Link Conditioner更改网络速度。这是一个很好的工具,因为它会强制你意识到当连接速度不够理想时应用程序会发生什么情况。
派遣组适合所有类型的队列。如果由于不想保留主线程而等待同步完成所有工作,则应该谨慎使用主队列上的调度组。然而,异步模型是一个有吸引力的方式来更新UI一旦几个长期运行的任务完成,如网络调用。
你目前的解决方案是好的,但总的来说,如果可能的话最好避免阻塞线程。您的下一个任务是重写相同的方法,以便在所有下载完成后异步通知您。
调度组,方案2
异步调度到另一个队列,然后阻止工作使用wait
是笨拙的。幸运的是,有一个更好的方法。DispatchGroup
可以在所有组的任务完成时通知您。
仍然在PhotoManager.swift中,用下面的代码替换里面的代码downloadPhotosWithCompletion(_:)
:
// 1
var storedError: NSError?
let downloadGroup = DispatchGroup()
for address in [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString] {
let url = URL(string: address)
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.sharedManager.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) { // 2
completion?(storedError)
}
以下是发生了什么事情:
- 在这个新的实现中,你不需要在
async
调用中包围方法,因为你没有阻塞主线程。 -
notify(queue:work:)
充当异步完成关闭。当组中没有剩余项目时调用它。您还指定您要安排在主队列上运行完成工作。
这是处理这个特定工作的更简洁的方式,因为它不会阻塞任何线程。
构建并运行应用程序。确认所下载的所有互联网照片下载完成后仍然显示:
并发循环
随着所有这些新工具的使用,您应该可以把所有东西都交给对手!
看看downloadPhotosWithCompletion(_:)
在PhotoManager
。您可能会注意到那里有一个for
循环,循环三次迭代并下载三个单独的图像。你的工作是看看你是否可以for
同时运行这个循环来尝试和加快速度。
这是一份工作DispatchQueue.concurrentPerform(iterations:execute:)
。它与for
循环的工作方式类似,它可以同时执行不同的迭代。它是同步的,只有在所有工作完成时才会返回。
在为给定工作量计算最佳迭代次数时必须小心。许多迭代和每次迭代的少量工作可能会产生太多的开销,从而使得调用并发的任何收益都无效。这种称为“ 跨步 ”的技术可以帮助你在这里。这是每次迭代都要完成多项工作的地方。
什么时候适合使用DispatchQueue.concurrentPerform(iterations:execute:)
?你可以排除串行队列,因为没有任何好处 - 你也可以使用正常的for
循环。对于包含循环的并发队列来说,这是一个很好的选择,尤其是在需要跟踪进度时。
在PhotoManager.swiftdownloadPhotosWithCompletion(_:)
中用下面的代码替换里面的代码:
var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) {
I in
let index = Int(i)
let address = addresses[index]
let url = URL(string: address)
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.sharedManager.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
前一个for
循环已被替换DispatchQueue.concurrentPerform(iterations:execute:)
为处理并发循环。
构建并运行应用程序。确认互联网下载功能仍然正常工作:
在设备上运行此新代码偶尔会产生稍微更快的结果。但是,所有这些工作值得吗?
其实在这种情况下不值得。原因如下:
- 你可能已经创造了更多的并行运行线程的开销,而不仅仅是首先运行
for
循环。您应该使用适当的步幅长度DispatchQueue.concurrentPerform(iterations:execute:)
来遍历非常大的集合。 - 您创建应用程序的时间有限 - 不要浪费时间预先优化您不知道的代码已损坏的代码。如果你要优化某些东西,优化一些值得注意并值得花时间的事情。通过在乐器中分析您的应用程序来查找执行时间最长的方法。查看如何在Xcode中使用仪器了解更多信息。
- 通常情况下,优化代码会使您的代码对于您和其他开发者而言更加复杂。确保增加的复杂功能是值得的。
请记住,不要为优化而疯狂。你只会让自己和其他必须通过代码遍历的人变得更加困难。
取消任务
到目前为止,您还没有看到允许您取消入队任务的代码。这是由哪个派生块对象表示的DispatchWorkItem
焦点。请注意,只能DispatchWorkItem
在到达队列头并开始执行之前取消它。
让我们通过从Le Internet的几个图像开始下载任务,然后取消其中的一些来演示这一点。
仍然在PhotoManager.swift中,将代码替换downloadPhotosWithCompletion(_:)
为以下内容:
var storedError: NSError?
let downloadGroup = DispatchGroup()
var addresses = [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString]
addresses += addresses + addresses // 1
var blocks: [DispatchWorkItem] = [] // 2
for i in 0 ..< addresses.count {
downloadGroup.enter()
let block = DispatchWorkItem(flags: .inheritQoS) { // 3
let index = Int(i)
let address = addresses[index]
let url = URL(string: address)
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.sharedManager.addPhoto(photo)
}
blocks.append(block)
DispatchQueue.main.async(execute: block) // 4
}
for block in blocks[3 ..< blocks.count] { // 5
let cancel = arc4random_uniform(2) // 6
if cancel == 1 {
block.cancel() // 7
downloadGroup.leave() // 8
}
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
以下是一段循序渐进的代码:
- 您展开
addresses
阵列以保存每个图像的三个副本。 - 您初始化一个
blocks
数组以保存调度块对象以备后用。 - 你创建一个新的
DispatchWorkItem
。你传递一个flags
参数来指定块应该从它被分派到的队列继承它的Quality of Service类。然后你可以定义要在闭包中完成的工作。 - 您将该块异步分派给主队列。在这个例子中,使用主队列可以更容易地取消选中的块,因为它是一个串行队列。设置调度块的代码已经在主队列上执行,因此您可以保证下次执行的时间不会超过。
- 通过切片
blocks
数组可以跳过前三个下载块。 - 在这里你
arc4random_uniform()
可以随机选择一个介于0和1之间的数字。这就像掷硬币一样。 - 如果随机数是1,则取消该块。这只能取消仍在队列中但尚未开始执行的块。块在执行过程中不能取消。
- 在这里,您记得从调度组中删除取消的块。
构建并运行应用程序,然后从Le Internet添加图像。您会看到该应用程序现在下载了三个以上的图像。每次重新运行应用程序时,额外图像的数量都会发生变化。队列中的某些其他图像下载在开始之前会被取消。
这是一个非常人为的例子,但它很好地说明了如何使用和取消分派块对象。
调度块对象可以做更多,所以一定要查看Apple的文档。
其他GCD乐趣
可是等等!还有更多!这里有一些额外的功能,距离挨打路径有点远。尽管您不会像使用这些工具那样频繁使用这些工具,但它们在正确的情况下会非常有帮助。
测试异步代码
这可能听起来像一个疯狂的想法,但是你知道Xcode具有测试功能吗?:]我知道,有时我喜欢假装它不在那里,但编写和运行测试在构建代码中的复杂关系时非常重要。
在Xcode中进行测试是在其子类中执行的,XCTestCase
并在其方法签名中以任何方式运行test
。测试是在主线程上进行测量的,因此您可以假定每个测试都以串行方式进行。
只要给定的测试方法完成,XCTest方法就会考虑完成测试并转到下一个测试。这意味着在下一次测试运行时,来自先前测试的任何异步代码将继续运行。
网络代码通常是异步的,因为您不想在执行网络提取时阻止主线程。再加上当测试方法结束时测试完成的事实,可能使测试网络代码变得困难。
我们来简单介绍一下测试异步代码的两种常用技术:一种使用信号量,另一种使用期望值。
信号灯
信号量是由一位如此谦虚的Edsger W. Dijkstra向世界介绍的老派线程概念。信号量是一个复杂的话题,因为它们建立在操作系统功能的复杂性之上。
如果您想了解有关信号量的更多信息,请查看关于信号量理论的详细讨论。如果你是学术类型的人,你可能想看看餐饮哲学家问题,这是一个使用信号量的经典软件开发问题。
打开GooglyPuffTests.swift并用下面的代码替换里面的代码downloadImageURLWithString(_:)
:
let url = URL(string: urlString)
let semaphore = DispatchSemaphore(value: 0) // 1
let _ = DownloadPhoto(url: url!) {
_, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
semaphore.signal() // 2
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)
if semaphore.wait(timeout: timeout) == .timedOut { // 3
XCTFail("\(urlString) timed out")
}
以下是上述代码中信号量的工作方式:
- 您创建一个信号并设置其起始值。这表示可以访问信号量而不需要增加信号量的事物的数量(注意递增信号量被称为信号量)。
- 您在完成关闭时发出信号灯信号。这会增加信号计数,并指示信号量可用于其他需要它的资源。
- 在给定的超时时间内,你等待信号量。这个调用会阻塞当前的线程,直到信号灯发出信号。此函数的非零返回码表示达到了超时。在这种情况下,测试失败,因为它认为网络不应该超过10秒才能返回 - 这是一个公平的点!
通过从菜单中选择Product \ Test或使用⌘+ U运行测试(如果您有默认的键绑定)。他们都应该及时获得成功:
禁用您的连接并再次运行测试。如果您正在设备上运行,请将其置于飞行模式。如果您正在模拟器上运行,那么只需关闭您的连接。10秒后测试完成,失败结果。很好,它工作!
这些都是相当简单的测试,但是如果你与服务器团队一起工作,那么这些基本测试可以防止指责哪些人应该承担最新的网络问题。
期望
该XCTest
框架以期望的形式为异步代码测试问题提供了另一种解决方案。这个功能可以让你设定一个期望 - 你期待的事情会发生 - 然后开始一个异步任务。然后,您可以让测试运行人员等待,直到异步任务将预期标记为已完成。
仍然在GooglyPuffTests.swift中downloadImageURLWithString(_:)
使用以下代码替换代码:
let url = URL(string: urlString)
let downloadExpectation =
expectation(description: "Image downloaded from \(urlString)") // 1
let _ = DownloadPhoto(url: url!) {
_, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
downloadExpectation.fulfill() // 2
}
waitForExpectations(timeout: 10) { // 3
error in
if let error = error {
XCTFail(error.localizedDescription)
}
}
以下是它的工作原理:
- 你通过创造期望
expectation(description:)
。测试运行器将在失败时在测试日志中显示字符串参数,因此请描述您期望发生的情况。 - 您调用
fulfill()
异步执行的闭包来标记期望已完成。 - 你等待期望通过打电话来实现
waitForExpectations(timeout:handler:)
。如果等待超时,则将其视为错误。
通过良好的网络连接运行测试。你应该看到Xcode控制台记录的总结如下:
测试套件'所有测试'于2016-12-01 02:32:57.179通过。 执行3次测试,在10.666(10.672)秒内发生0次失败(0次意外)
现在禁用您的网络连接并重新运行测试。您应该看到一条错误消息和记录的摘要结果,如下所示:
[GooglyPuffTests.GooglyPuffTests testLotsOfFacesImageURL]:失败 - http://i.imgur.com/tPzTg7A.jpg失败。互联网连接似乎处于脱机状态。 ... 测试套件“所有测试”在2016-12-01 02:35:10.055失败。 执行3次测试,在0.061(0.082)秒内发生3次失败(0次意外)
最终结果与使用信号量没有太大区别,但利用XCTest
框架是一个更清晰,更易读的解决方案。
调度来源
调度源是GCD的一个特别有趣的功能。调度源基本上可以用于监视某种类型的事件。事件可以包括Unix信号,文件描述符,Mach端口,VFS节点和其他不明确的东西。
在设置调度源时,可以告诉它要监视的事件类型以及应该执行其事件处理程序块的调度队列。然后,您将一个事件处理程序分配给调度源。
创建时,调度源以暂停状态开始。这允许进行额外的配置步骤,例如设置事件处理程序。一旦你配置了你的调度源,你应该继续它来开始处理事件。
在本教程中,您将通过以一种相当特殊的方式使用调度源来体验一下小小的乐趣:监视应用程序何时进入调试模式。
打开PhotoCollectionViewController.swift并在backgroundImageOpacity
全局属性声明下方添加以下内容:
#if DEBUG // 1
var signal: DispatchSourceSignal? // 2
private let setupSignalHandlerFor = { (_ object: AnyObject) -> Void in // 3
let queue = DispatchQueue.main
signal =
DispatchSource.makeSignalSource(signal: Int32(SIGSTOP), queue: queue) // 4
signal?.setEventHandler { // 5
print("Hi, I am: \(object.description!)")
}
signal?.resume() // 6
}
#endif
代码有一点涉及,所以请逐步浏览它:
- 您只能在DEBUG模式下编译此代码,以防止“感兴趣的各方”对您的应用程序有更多的了解。:]通过在项目设置 - >构建设置 - > Swift编译器 - 自定义标志 - >其他Swift标志 - >调试下添加-D DEBUG来定义DEBUG。它应该已经在启动器项目中设置。
- 您声明了一个用于监视Unix信号
signal
的类型变量DispatchSourceSignal
。 - 您可以创建一个分配给
setupSignalHandlerFor
全局变量的块,用于一次性设置调度源。 - 在这里你设置
signal
。您指出您有兴趣监视SIGSTOP
Unix信号并处理主队列中收到的事件 - 您会很快发现它的原因。 - 如果调度源成功创建,则注册一个事件处理程序闭包,每当您收到
SIGSTOP
信号时都会调用它。您的处理程序打印包含类描述的消息。 - 所有源默认情况下都处于暂停状态。在这里,您告诉调度源恢复,以便它可以开始监视事件。
将以下代码添加到viewDidLoad()
呼叫的下方super.viewDidLoad()
:
#if DEBUG
_ = setupSignalHandlerFor(self)
#endif
该代码调用调度源的初始化代码。
构建并运行应用程序。暂停程序执行并通过点击暂停然后在Xcode调试器中播放按钮来立即恢复应用程序:
检查控制台。你应该看到这样的东西:
Hi, I am:<GooglyPuff.PhotoCollectionViewController:0x7fbf0af08a10>
你的应用程序现在可以调试!这真是太棒了,但你如何在现实生活中使用它?
无论何时恢复应用程序,您都可以使用它来调试对象并显示数据。当恶意攻击者将调试器附加到您的应用程序时,您也可以为您的应用程序定制安全逻辑以保护自己(或用户的数据)。
一个有趣的想法是将此方法用作堆栈跟踪工具来查找要在调试器中操作的对象。
考虑一下这种情况。当你彻底停止调试器时,你几乎不会在期望的堆栈帧上。现在您可以随时停止调试器并在您想要的位置执行代码。如果您想要在应用程序中的某个点执行代码,而这些代码很难从调试器访问,那么这非常有用。试试看!
在刚刚添加print()
的setupSignalHandlerFor
块内的语句上放置一个断点。
在调试器中暂停,然后重新开始。该应用程序将达到您添加的断点。你现在深入你的PhotoCollectionViewController
方法的深处。现在你可以访问PhotoCollectionViewController
你心中内容的实例。非常方便!
注意:如果您还没有注意到调试器中的哪些线程,请立即查看它们。主线程将始终是libdispatch(GCD的协调器)作为第二个线程的第一个线程。之后,线程数和剩余线程取决于应用程序到达断点时硬件的功能。
在调试器控制台中,输入以下内容:
(lldb)expr object.navigationItem.prompt =“WOOT!”
Xcode调试器有时可能不合作。如果您收到消息:
错误:使用未解析的标识符'self'
然后,你必须努力解决LLDB中的错误。首先记下object
调试区域中的地址:
(lldb)po对象
然后手动将值转换为所需的类型:
lldb)expr让$ vc = unsafeBitCast(0x7fbf0af08a10,为:GooglyPuff.PhotoCollectionViewController.self) (lldb)expr $ vc.navigationItem.prompt =“WOOT!”
现在恢复执行应用程序。你会看到以下内容:
盛大中央派遣教程使用这种方法,您可以更新UI,查询类的属性,甚至执行方法 - 而无需重新启动应用程序即可进入该特殊工作流程状态。很简约。
然后去哪儿?
您可以在此下载完成的Grand Central Dispatch教程项目。
我讨厌再次讨论这个问题,但你真的应该看看如何使用仪器教程。如果您打算对应用程序进行任何优化,您一定会需要这个。请注意,Instruments很适合分析相对执行情况:比较哪些代码区域相对于其他区域需要更长的时间。如果你想弄清楚某个方法的实际执行时间,那么你可能需要提出一个更加自制的解决方案。
另请参阅如何在Swift中使用NSOperations和NSOperationQueue教程,这是一种构建于GCD之上的并发技术。一般来说,如果您正在使用简单的“即燃即用”任务,则最好使用GCD。NSOperations提供了更好的控制,一个处理最大并发操作的实现,以及一个以速度为代价的更面向对象的范例。
您还应该看看我们的iOS并发与GCD和Operations视频教程系列,其中涵盖了本教程中介绍的许多相同的主题。
请记住,除非您有特定的理由要降低,否则请务必坚持使用更高级别的API。如果你想了解更多,或者想要做一些真正有趣的事情,只有冒险进入苹果的黑暗艺术。:]
祝好运并玩得开心点!在下面的讨论中发布任何问题或反馈!
网友评论