在Swift中应用Grand Central Dispatch 下
from:http://www.cocoachina.com/swift/20150130/11054.html
4
3430
本文由loveltyoic(博客)翻译自raywenderlich,原文:Grand Central Dispatch Tutorial for Swift: Part 1/2 欢迎来到本GCD教程的第二同时也是最终部分! 在第一部分中,你学到了并发,线程以及GCD的工作原理。通过使用dispatch_barrrier和dispatch_sync,你做到了让PhotoManager单例在读写照片时是线程安全的。除此之外,你用到dispatch_after来提示用户,优化了用户体验。还有,使用dispatch_async异步执行CPU密集型任务,从而为视图控制器初始化过程减负。 如果你跟着教程做,现在可以从第一部分的示例工程继续。如果你没有完成第一部分或不想再用你的工程,可以下载第一部分的完成文件。 是时候进一步探索GCD了! 纠正过早出现的弹窗 你可能注意到,当你通过 Le Internet 选项添加照片时,会有提示框在图片下载完成之前就弹出,如下图:
错误在于 PhotoManager 里的 downloadPhotosWithCompletion:
这里在方法的最后调用completion闭包——你会想当然的认为所有图片都下载完了。但不幸的是,在此时无法保证。 DownloadPhoto类的实例方法从一个URL下载图片并且不等下载完成就立即退出。换言之,downloadPhotosWithCompletion在最后调用completion闭包,就好像其中的所有方法都在顺序执行,并且在每个方法完成后才执行下一个。 然而,DownloadPhoto(url:)是异步并且立即返回的——所以目前的方式不能正常工作。 downloadPhotosWithCompletion应该在所有图片下载任务都完成后再调用自己的completion闭包。问题是:你怎么监视并发的异步事件呢?你不知道它们何时完成,以何种顺序。 也许你可以用多个Bool值来追踪下载情况,但那不容易扩展。而且坦白讲,那是很丑陋的代码。 幸运的是,dispatch groups就是专为监视多个异步任务的完成情况而设计的。 调度组(Dispatch Groups) 调度组在一组任务都完成后会发出通知。这些任务可以是异步或同步的,甚至可以分布在不同的队列。调度组还可以通过同步或异步的方式来通知。因为任务在不同的队列中,disptch_group_t实例用来追踪队列中的不同任务。 在组内所有事件都完成时,GCD API提供了两种方式发送通知。 第一种是dispatch_group_wait,它会阻塞当前进程,直到所有任务都完成或是等待超时。这正是我们的例子中需要的方式。 打开 PhotoManager.swift ,替换downloadPhotosWithCompletion:
逐一来看注释:
运行app,下载几张图片,留意你的app是如何表现的。 Note:如果网速太快以至于分辨不出何时执行的闭包,你可以修改设备的设置。在 Setting 中的Developer Section 。打开 Network Link Conditioner,选择“Very Bad Network”。 如果在模拟器上,用工具变更网速。这是你武器库中一个很好的工具,它让你清楚在不佳的网络下你的app会发生什么。 这个方案目前不错,但最好能避免阻塞进程。你下一步的工作是重写这个方法来异步通知下载完成。 在学习下一个调度组的用法前,先看看怎样在不同的队列类型下使用调度组。
调度组,再来一次 做的不错,但是异步调度到另一个队列然后用 dispatch_group_wait 阻塞还是有一些笨拙。还有另一种方式… 在 PhotoManager.swift 中找到downloadPhotosWithCompletion并替换之:
异步方法是如何工作的:
这是更优雅的方法,并且不会阻塞任何进程。 并发过多带来的危险 通过支配这些新工具,你应该将每件事都线程化,对吗?
看看PhotoManager中的downloadPhotosWithCompletion。你会发现通过for循环下载了三张图片。现在来看看能否通过并发执行for循环来提速。 是时候请出dispatch_apply了。 dispatch_apply像for循环一样,只不过它会并发地执行循环过程。这个函数是同步的,所以像普通的for循环一样,dispatch_apply在所有工作都完成后才返回。 要注意循环的最佳次数,如果有太多循环但每个循环内只有很小的工作量,那么额外的开销会抹杀掉并发带来的好处。 步进 (striding)可以帮助到你。它让你在每次循环中做多件工作。 什么时候用dispatch_apply合适?
替换downloadPhotosWithCompletion如下: 现在你的循环可以并发执行了;调用 dispatch_apply 时,第一个参数是循环的次数,第二个参数是执行任务的队列,第三个参数是闭包。 尽管你的代码在添加图片时是线程安全的,但是图片的顺序取决于线程完成的顺序。 运行app,用 Le Internet 添加一些图片,发现不同了吗? 在真机上运行新的代码会发现 些许 的速度提升。但是这值得吗? 实际上,在这里并不值得这么做。原因如下:
记住,不要痴迷于优化。否则只会让你自己为难,也让看你代码的人抓狂。 取消调度块 iOS 8 和 OS X Yosemite引入了 调度对象块 (dispatch block object)。它们实现起来就像对闭包再包装一层。调度对象块可以做到很多事情,比如为队列中的对象设置QoS等级来决定优先级,但最显著的能力是可以取消块的执行。要明白对象块只有在轮到它执行之前才可以取消(一旦开始执行就不能取消了)。 为了说明这个问题,首先用 Le Internet 下载一些图片,然后取消它们。替换 PhotoManager.swift 中的downloadPhotosWithCompletion:
运行,从 Le Internet 添加图片。你会看到app下载3张图片,以及随机数量的额外图片。那些没下载的图片是因为在加入队列 后 被取消了。这是一个刻意设计的例子,但是很好的演示了怎样使用调度对象块以及如何取消它。 调度对象块能做更多事情,别忘了查看文档。 五花八门的GCD趣用 等等!还有更多!下面展示一些常规用途之外的功能。尽管你不会经常使用这些工具,但他们可能在特定情况下非常有用。 测试异步代码 这听起来很疯狂,但是你知道Xcode拥有测试功能吗?:]我知道,有时我喜欢假装它不存在,但是编写和运行测试对构建复杂的代码很重要。 Xcode中的测试运行在XCTestCase的子类之下,它会运行所有以test开头的方法。测试跑在主线程下,所以你可以认为测试是顺序执行的。 一旦给定的测试方法返回了,XCTest 会认为这个测试完成了而去做下一个测试。这就是说,在下一个测试执行过程中,前一个测试中的异步代码也在继续执行。 网路请求通常是异步的,因为你不想阻塞主线程。一旦测试方法返回,测试也就结束了,因此很难对网络请求做测试。 我们简单看一下两种普遍的测试异步代码的方法:信号量(semaphores)和 期望(expectations)。 信号量 信号量是一个古老学院派的线程概念,它是由谦逊的Edsger W. Dijkstra提出的。信号量是很复杂的话题,因为它建立在错综复杂的操作系统函数之上。 如果你想了解更多信号量的知识,查阅细说信号量原理。如果你是学院派,有一个用到了信号量的经典软件开发问题叫做哲学家进餐问题。 信号量让你控制多个消费者对有限资源的获取。例如,如果你创建一个信号量来控制拥有2个资源的资源池,那么同一时刻最多有两个线程可以进入临界区。其它也想使用资源的线程必须在FIFO队列中等待。 打开 GooglyPuffTests.swift 并替换掉 downloadImageURLWithString:
以上代码中信号量的工作原理: Product/Test 或 cmd+U 运行测试。测试应该成功。 断掉网络连接并再次测试;如果在真机测试,请开启飞行模式。如果在模拟器上,直接断网就好了。测试在10秒后会返回失败的结果。很好,起作用了! 这是相当微不足道的测试,但是如果你和服务端团队一起工作,这些基础测试可以避免一些涉及网络问题的无端指责。 期望(expectations) XCTest框架提供了另一种使用 期望 来测试异步代码的方法。这种特性让你首先设置你的期望——你希望发生的事——然后再开始异步任务。接下来测试会一直等待,直到异步任务将期望标记为 已完成 。 替换 GooglyPuffTests.swift 中的downloadImageURLWithString:
工作原理: 运行测试。结果和使用信号量没什么不同,但使用XCTest框架是更清晰易读的方案。 调度源(Dispatch Sources) GCD中存在一个特别有趣的特性叫调度源,它是一个包含底层功能的百宝囊,帮助你响应或监控Unix信号,文件描述符(file descriptors),Mach端口,VFS Nodes,以及其他复杂的东西。所有这些都超出了本教程的范围,但是你可以尝试着使用一下调度源对象。 第一次使用调度源的用户可能会迷失其中,所以你首先要理解dispatch_source_create的工作原理。下面是创建它的函数原型:
第一个参数type: dispatch_source_type_t是最重要的参数,因为它描述了句柄(handle)和掩码(mask)参数。你需要查看Xcode文档来弄清楚dispatch_source_type_t的参数有哪些可选项。 这里你会监视DISPATCH_SOURCE_TYPE_SIGNAL。如文档所述: 调度源监控当前进程的信号。句柄(handle)是信号数字(int)。掩码(mask)没用到(传0)。 Unix信号列表可以从signal.h找到。在顶部有一串#define。在这些信号列表中,你将要监控SIGSTOP信号。这个信号会在进程接收到不可抗拒的挂起指令时被发送。这个信号与你用LLDB debugger调试程序时发送的信号相同。 进入 PhotoCollectionViewController.swift ,在viewDidLoad附近添加下面的代码。你需要为类添加两个私有属性,并在viewDidLoad的开始处添加段代码,在调用superclass和ALAssetLibrary之间:
这段代码有点难懂,因此逐个注释来讲解: 运行app;暂停调试器然后立即恢复。检查控制台(console),你会看到类似下面的信息:
你的app现在可以感知到调试(debugging-aware)了!这真棒,但在现实中怎样用它呢? 你可以用它调试一个对象并在恢复app时展示数据;你也可以自定义一些安全逻辑来保护app,当恶意攻击者在你的程序上附着调试器的时候。 有趣的想法是把这个方法当做堆栈追踪工具,来找到你想要在调试器中修改的对象。
设想一下这样的场景。当你意外地停掉调试器时,你很难处在期望的栈帧上。而现在你可以在任意时刻停止调试器并让代码执行到你期望的位置。这很有用,当你想执行一段从调试器很难达到的代码。试一试! 在viewDidLoad中的NSLog语句处设置断点。暂停调试器,然后再开始;app会命中你刚刚设置的断点。现在你已经深入到PhotoCollectionViewController方法中了。现在你可以随心所欲地使用PhotoCollectionViewController实例了。多么便捷! 注意:如果在调试器中你不知道哪个线程是哪个,来看一下。主线程总是第一个,libdispatch,GCD的协调器是第二个。剩下的线程要看硬件当时在做什么样的工作。 在调试器中,输入: 然后继续执行app。你会看到如下所示:
通过这个方法,你可以更新UI,探查类的属性,甚至执行方法——无需重启app来进入特定的工作流状态。很巧妙。 下一步? 下载最终的工程。 我不想重提,但是你真的应该看一下怎样使用Instruments。如果你想优化app,绝对需要这个。Instruments可以概述程序中哪些代码相对其它代码执行更久。如果你想知道代码实际的执行时间,很可能需要一些自制的解决方案。 同时学习如何在Swift中使用NSOperations和NSOperationQueue,一种基于GCD的并发技术。实际上,这是使用GCD的最佳实践。NSOperations提供更好的控制,处理最多的并发操作,在牺牲一定速度的情况下更加面向对象。 记住,除非你有特别的理由深入底层,你应该始终尝试并坚持使用更高层的API。只在你想学习更多或做一些非常非常“有趣”的事时才进入到Apple的“暗黑艺术”(dark art)中探险。:] 祝你好运,尽情欢乐! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |