欢迎来到Grand Central Dispatch系列教程的第二部分!
在教程的第一部分,你学到了一些关于并发,线程及GCD工作原理的知识。你通过并用dispatch_barrier_async与dispatch_sync保证了PhotoManager单例在读取与写入照片过程中的线性安全性。值得一提的是,你不仅通过dispatch_after及时地向用户发出提醒以此优化了App的UX而且还通过dispatch_async将部分工作从一个View Controller的实例化过程中分割至另一线程以此实现CPU高密度处理工作。
假如你是一路从上一部分教程学过来的话,你完全可以在以前的工程文件上继续Coding。但假如你没有完成教程的第一部分或是不想继续使用自己的工程文件的话,你可从这里下载到教程第一部分的完整工程文件。
OK! 是时候探索一下更多关于GCD的知识了。
修复提早出现的Popup
也许你已经注意到了当你通过Le Internet的方式添加照片时,在所有照片下载完成前AlertView就已经跳出来提醒你“Download Complete”。
See That?
其实问题出在PhotoManaer的downloadPhotosWithCompletion函数中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
var
storedError: NSError!
for
address
in
[OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString] {
let url = NSURL(string: address)
let photo = DownloadPhoto(url: url!) {
image, error
in
if
error != nil {
storedError = error
}
}
PhotoManager.sharedManager.addPhoto(photo)
}
if
let completion = completion {
completion(error: storedError)
}
}
|
在函数结尾部分调用completion闭包--这就代表着你认为所有的照片的下载任务都已经完成。但不幸的是此时此刻你并无法保证所有的下载任务都已经完成。
DownloadPhoto类的实例化方法开始从一个URL下载文件并在下载完成前立即返回值。换句话说,downloadPhotosWithCompletion在函数结尾处调用其自己的completion闭包就好像它自己就是个有着直线型同步执行代码的方法体,并且每个方法执行完自己的工作后都会调用这个completed。
不管怎样,DownloadPhoto(url:)是异步执行的并且立即返回--所以这个解决方案不管用。
再有,downloadPhotosWithCompletion应该在所有的照片下载任务都调用了completion闭包后再调用自己的completion闭包。那么问题来了:你怎样去监管那些同时执行的异步事件呢?你根本不会知道它们会何时并以何种顺序结束。
或许你可以写多个Bool值去跟踪每个任务的下载状态。说实话,这样做的话会感觉有些low并且代码看起来会很乱。
万幸的是,派发组(dispatch groups)正是为满足监管这种多异步completion的需要所设计的。
派发组(Dispatch Group)
当整组的任务都完成时派发组会提醒你。这些任务既可以是异步的也可以是同步的并且可以在不同队列中被监管。当全组任务完成时派发组可以通过同步或异步的方式提醒你。只要有任务在不同队列中被监管,dispatch_group_t实例便会在多个队列中的持续监管这些不同的任务。
当组中的全部任务执行完毕后,GCD的API提供两种方式向你发出提醒。
第一个便是dispatch_group_wait,这是一个在组内所有任务执行完毕前或在处理超时的情况下限制当前线程运行的函数。在AlertView提早出现的情况下,使用disptach_group_wait绝对是你的最佳解决方案。
打开PhotoManager.swift并用如下代码替换原downloadPhotosWithCompletion函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
dispatch_async(GlobalUserInitiatedQueue) {
// 1
var
storedError: NSError!
var
downloadGroup = dispatch_group_create()
// 2
for
address
in
[OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]
{
let url = NSURL(string: address)
dispatch_group_enter(downloadGroup)
// 3
let photo = DownloadPhoto(url: url!) {
image, error
in
if
let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
// 4
}
PhotoManager.sharedManager.addPhoto(photo)
}
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER)
// 5
dispatch_async(GlobalMainQueue) {
// 6
if
let completion = completion {
// 7
completion(error: storedError)
}
}
}
}
|
代码分步解释:
-
一旦使用限制当前线程运行的同步式dispatch_group_wait,你必须用dispatch_async将整个方法调至后台队列以保证主线程的正常运行。
-
在这里声称了一个你可以将其视为未完成任务计数器的新派发组。
-
dispatch_group_enter用来向派发组提醒新任务执行的开始。你必须使调用dispatch_group_enter的次数要相称于调用dispatch_group_leave的次数,不然将会导致App崩溃。
-
在这里,你向派发组提醒任务的执行结束。再强调一遍,进出派发组的次数一定要相等。
-
在所有任务执行结束后或者在处理超时的情况下dispatch_group_wait才会执行。假如在所有任务执行结束前就出现了处理超时的情况,函数便会返回一个非零结果。你可以将其放在一个特殊的闭包中以检查是否会发生处理超时的情况。当然,在本教程的情况下你可以使用DISPATCH_TIME_FOREVER令其保持等待请求状态,这就意味它会一直等下去,因为照片的下载任务总会完成的。
-
到目前为止,你保证了照片下载任务要么顺利完成要么出现处理超时的情况。其后你便可以返回至主队列运行你的completion闭包。这将向主线程添加稍后将被执行的任务。
-
条件允许的情况下执行completion闭包。
编译并运行你的App,你会发现在点击下载照片的选项后你的completion闭包将会在正确的时间执行。
提醒:当你在实体设备上运行App的时候,假如网络机制运行过快以至于你无法判断的completion闭包开始执行时间的话,你可以到App的Setting中的Developer Section中进行一些网络调整。打开Network Link Conditioner,选择Very Bad Network是一个不错的选择。
假如你在模拟器上运行App的话,你可以通过使用Network Link Conditioner included in the Hardare IO Tools for Xcode调整你的网络速度。这是一个当你需要了解在网络状况不好的情况下App执行情况的绝佳工具。
这个解决方法的好处不止于此,但总体上来说它在大多数情况下避免了限制线程正常运行的可能。你接下来的任务便是写一个相同的并以异步的方式向你发出'照片下载完成'提醒的方法。
在开始之前先了解一下对于不同类型的队列来说应该何时使用并怎样使用派发组的简短教程。
-
自定义连续队列(Custom Serial Queue): 在组内任务全部完成时需要发出提醒的情况下,自定义连续队列是一个不错的选择。
-
主队列(Main Queue[Serial]):在当你以同步的方式等待所有任务的完成且你也不想限制主队列的运行的情况下你应该在主线程上警惕使用它。但比如像网络请求这种长时间运行的任务结束时异步模型是用来更新UI的绝佳方式。
-
并发队列(Concurrent Queue):这对于派发组及完成提醒也是个不错的选择。
派发组,再来一次!
出于精益求精的目的,通过异步的方式将下载任务派发到另一个队列并用dispatch_group_wait限制其运行的做法是不是有些stupid呢?试试另一种方法吧...
用如下实现代码代替PhtotManager.swift中的downloadPhotosWithCompletion函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
// 1
var
storedError: NSError!
var
downloadGroup = dispatch_group_create()
for
address
in
[OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]
{
let url = NSURL(string: address)
dispatch_group_enter(downloadGroup)
let photo = DownloadPhoto(url: url!) {
image, error
in
if
let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
}
PhotoManager.sharedManager.addPhoto(photo)
}
dispatch_group_notify(downloadGroup, GlobalMainQueue) {
// 2
if
let completion = completion {
completion(error: storedError)
}
}
}
|
这就是你的新异步方法的工作原理:
-
在这个新的实现方法中,当你不再限制主线程的时候你就没有必要将其放进dispatch_async的调用中。
-
dispatch_group_notify相当于一个异步completion闭包。当派发组中不再剩余任何任务且轮到completion闭包运行时,这段代码将会执行。你也可以定义你的completion代码在哪个队列上运行。在这段代码中你便要运行在主队列上。
对于在不限制任何线程运行的情况下处理这种特殊需求的例子来说,这是一种较为简洁的方式。
过多使用并发机制造成的危险
学了这么多的新东西后,你是不是该令你的代码全部实现线程化呢?
Do It !!!
看看你在PhotoManger中的downloadPhotosWithCompletion函数。你应该注意到了那个循环在三个参数间并下载三张不同照片的for循环。你接下来的工作便是尝试通过并发机制加快for循环的运行速度。
该轮到dispatch_apply上场了。
dispatch_apply就像是一个以并发的形式执行不同迭代过程的for循环。就像是一个普通的for循环,dispatch_apply是一个同步运行且所有工作完成后才会返回的函数。
当你在对闭包内已给定任务的数量进行最优化迭代过程数量的设定时一定要当心,因为这种存在多个迭代过程且每个迭代过程仅包含少量工作的情况所消耗的资源会抵消掉并发调用所产生的优化效果。这个叫做striding的技术会在你处理多任务的每个迭代过程的地方帮到你。
什么时候适合用dispatch_apply呢?
-
自定义连续队列(Custome Serial Queue):对于连续队列来说,dispatch_apply没什么用处;你还是老实地用普通的for循环吧。
-
主队列(Main Queue[Serial]):跟上面情况一样,老实地用普通for循环。
-
并发队列(Concurrent Queue):当你需要监管你的任务处理进程时,并发循环绝对是一个好主意。
回到downloadPhotosWithCompletion并替换成如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
var
storedError: NSError!
var
downloadGroup = dispatch_group_create()
let addresses = [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]
dispatch_apply(addresses.count, GlobalUserInitiatedQueue) {
i
in
let index = Int(i)
let address = addresses[index]
let url = NSURL(string: address)
dispatch_group_enter(downloadGroup)
let photo = DownloadPhoto(url: url!) {
image, error
in
if
let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
}
PhotoManager.sharedManager.addPhoto(photo)
}
dispatch_group_notify(downloadGroup, GlobalMainQueue) {
if
let completion = completion {
completion(error: storedError)
}
}
}
|
你的循环块现在就是以并发的形式运行;在上述代码中,你为调用dispatch+apply提供了三个参数。第一参数声明了迭代过程的数量,第二个参数声明了将要运行多个任务的队列,第三个参数声明了闭包。
要知道尽管你已经有了在线程安全模式下添加照片的代码,但是照片顺序会根据最先完成的线程的顺序所排列。
编译并运行,通过Le Internet的方式添加一些照片。注意到有什么不同吗?
在真实设备上运行修改后的代码偶尔会运行得快一些。所以,上面做出的修改值得吗?
其实,在这种情况下它不值得你这么做。原因如下:
-
你已经调用出了比for循环再同种情况下消耗更多资源的线程。dispatch_apply在这里显得有些小题大做了。
-
你写App的时间是有限的--不要为那些'抓鸡不成蚀把米'的优化代码浪费时间,把你的时间用在优化得有明显效果的代码上。你可以选择使用Xcode中的Instruments来测试出你App中执行时间最长的方法。
-
在某些情况下,优化后的代码甚至会增加你和其他开发者理解其逻辑结构的难度,所以优化效果一定要是物有所值的。
记住,不要痴迷于优化,要不然你就是和自己过不去了。
取消派发块(dispatch block)的执行
要知道在iOS 8和OS X Yosemite中加入了名为dispatch block objects的新功能(中文叫‘派发块对象’感觉总是怪怪的,所以就继续用英文原名)。Dispatch block objects可以做不少事儿了,比如通过为每个对象设定一个QoS等级来决定其在队列中的优先级,但它最特别的功能便是取消block objects的执行。但你需要知道的是一个block object只有在到达队列顶端且开始执行前才能被取消。
咱们可以通过‘利用Le Internet开始并再取消照片下载任务’的方式详细描述取消Dispatch Block的运行机制。用下述代码代替PhotoManager.swift中的downloadPhotosWithCompletion函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
var
storedError: NSError!
let downloadGroup = dispatch_group_create()
var
addresses = [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]
addresses += addresses + addresses
// 1
var
blocks: [dispatch_block_t] = []
// 2
for
i
in
0 ..< addresses.count {
dispatch_group_enter(downloadGroup)
let block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {
// 3
let index = Int(i)
let address = addresses[index]
let url = NSURL(string: address)
let photo = DownloadPhoto(url: url!) {
image, error
in
if
let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
}
PhotoManager.sharedManager.addPhoto(photo)
}
blocks.append(block)
dispatch_async(GlobalMainQueue, block)
// 4
}
for
block
in
blocks[3 ..< blocks.count] {
// 5
let cancel = arc4random_uniform(2)
// 6
if
cancel == 1 {
dispatch_block_cancel(block)
// 7
dispatch_group_leave(downloadGroup)
// 8
}
}
dispatch_group_notify(downloadGroup, GlobalMainQueue) {
if
let completion = completion {
completion(error: storedError)
}
}
}
|
-
addresses数组内包含每个都被复制了三份的address变量。
-
这个数组包含着晚些时候将被使用的block objects。
-
dispatch_block_create声明了一个新的block object。第一个参数是定义了不同block特性的标志。这个标志使得block继承了其在被分配至队列中的QoS等级。第二个参数是以一个闭包形式定义的block。
-
这是一个以异步形式分发到全局主队列的block。在这个例子中所使用的主队列是一个连续队列,所以其更容易取消所选的blocks。定义了分发blocks的代码已在主队列上的执行保证了下载blocks的稍后执行。
-
除去前三次的下载,在剩余的数组元素中执行for循环。
-
arc4random_uniform提供一个在0至上限范围内(不包含上限)的整数。就像掷硬币那样,将2设定为上限后你将会得到0或1中的某一个整数。
-
假如在随机数是1、block还在队列中且没有正在被执行的情况下,block则被取消。在执行过程中的block是不能被取消的。
-
当所有blocks都被加入分发队列后,不要忘记删除被取消的队列。
编译并运行App,通过Le Internet 的方式添加照片。你会发现App在下载了原来的三张照片后还会再下载一个随机数量的照片。分配至队列后,剩余的blocks将被取消。尽管这是一个很牵强的例子但起码它很好的描述了dispatch block objects如何被使用或被取消的。
Dispatch block objects能做的还有很多,使用前别忘了看看官方文档。
GCD带来的各种各样的乐趣
且慢!再容我讲点儿东西!其实这还有些不常用的函数,但在特殊情况下它们是非常有用的。
测试异步代码
这也许听起来有些不靠谱,但你知道Xcode上的确有这项测试功能吗?:]其实在某些情况下我是假装不知道有这项功能的,但是在处理具有复杂关系的代码的时候,代码编写与运行的测试是非常重要的。
Xcode中的测试功能是以XCTestCase的子类形式出现其且在其中运行的任何方法都是以test开头出现的。测试功能在主线程上运行,所以你可以假设每个测试都是以一种连续(serial)的方式运行的。
只要一个给定的测试方法完成了执行,XCTest方法就会认定一个测试已经完成并开始下一个测试,这就意味着在新的测试运行的同时,上一个测试中的异步代码还会继续运行。
当你在执行一个网络请求任务且不想限制主线程的运行时,那么这类网络任务通常是以异步方式执行的。这种“测试方法的结束代表着整个测试过程的结束”的机制加大了网络代码测试的难度。
别紧张,接下来咱们看两个常用的且专门用来测试以异步方式执行的代码的技术:一个使用了信号量(semaphores),另一个使用了期望(expectations)。
信号量(Semaphores)
在很多学校的OS课中,一提到大名鼎鼎的Edsger W.Dijkstra时肯定会讲到信号量这个跟线程相关的概念。信号量难懂之处在于它建立在那些复杂的操作系统的函数之上。
假如你想学习更多关于信号量的知识,请到这里了解更多关于信号量理论的细节。假如你是个专注于学术研究的家伙,从软件开发的角度来看,关于信号量的经典例子肯定就是哲学家进餐问题了。
信号量适用于让你在资源有限的情况下控制多个单位的资源消耗。举个例子,假如你声明了一个其中包含两个资源的信号量,在同一时间内最多只能有两个线程访问临界区。其他想使用资源的线程必须以FIFO(First Come, First Operate)的顺序在队列中等待。
打开GooglyPuffTests.swift并用如下代码替换downloadImageURLWithString函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func downloadImageURLWithString(urlString: String) {
let url = NSURL(string: urlString)
let semaphore = dispatch_semaphore_create(0)
// 1
let photo = DownloadPhoto(url: url!) {
image, error
in
if
let error = error {
XCTFail(
"\(urlString) failed. \(error.localizedDescription)"
)
}
dispatch_semaphore_signal(semaphore)
// 2
}
let timeout = dispatch_time(DISPATCH_TIME_NOW, DefaultTimeoutLengthInNanoSeconds)
if
dispatch_semaphore_wait(semaphore, timeout) != 0 {
// 3
XCTFail(
"\(urlString) timed out"
)
}
}
|
以下便是信号量如何在上述代码中工作的解释:
-
创建信号量。参数表明了信号量的初始值。这个数字代表着可以访问信号量线程的数量,我们经常以发送信号的方式来增加信号量。
-
你可以在completion闭包中向信号量声明你不再需要资源了。这样的话,信号量的值会得到增加并且向其他资源声明此时信号量可用。
-
设定信号量请求超时的时间。在信号量声明可用前,当前线程的运行将被限制。若出现超时的话,函数将会返回一个非零的值。在这种情况下测试便是失败的,因为它认为网络请求的返回不该使用超过十秒的时间。
通过使用菜单中的Product/Test选项或者使用?+U快捷键测试App的运行。
断开网络连接后再次运行测试;假如你在实机上运行就打开飞行模式,若是模拟器的话就断开链接。10秒后这次测试便以失败告终。
假如你是一个服务器团队中一员,完成这些测试还是挺重要的。
期望(Expectations)
XCTest框架提供了另一个用来测试异步方式执行代码的解决方案,期望。这便允许你在一个异步任务开始执行前设定一个期望--一些你期待发生的事。在异步任务的期望被标记为已完成(fulfilled)前,你可以令test runner一直保持等待状态。
用以下代码代替GooglyPufftests.swift中的downloadImageWithString函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func downloadImageURLWithString(urlString: String) {
let url = NSURL(string: urlString)
let downloadExpectation = expectationWithDescription(
"Image downloaded from \(urlString)"
)
// 1
let photo = DownloadPhoto(url: url!) {
image, error
in
if
let error = error {
XCTFail(
"\(urlString) failed. \(error.localizedDescription)"
)
}
downloadExpectation.fulfill()
// 2
}
waitForExpectationsWithTimeout(10) {
// 3
error
in
if
let error = error {
XCTFail(error.localizedDescription)
}
}
}
|
解释一下:
-
通过expectationWithDescription参数声明了一个期望。当测试失败的时候,test runner将会在Log中显示这段字符串参数,以此代表着你所期待发生的事情。
-
调用以异步方式标记期望已完成的闭包中的fulfill.
-
调用的线程等待期望被waitForExpectationsWithTimeout函数标记完成。若等待超时,线程将被当做一个错误。
编译并运行测试。尽管测试结果跟使用信号量机制比起来并没有太多的不同,但这却是一种使XCTest框架更加简洁易读的方法。
派发源(Dispatch Sources)的使用
GCD的一个非常有趣的特性就是派发源,它是包含了很多低层级别的功能。这些功能可以帮你对Unix的信号,文件描述符,Mach端口、VFS Nodes进行反馈以及检测。尽管这些东西超出了这篇教程的范围,但我觉得你还是要试着去实现一个派发源对象。
很多派发源的初学者经常被卡在如何使用一个源的的问题上,所以你要清楚dispatch_source_create的工作原理。下面的函数声明了一个源:
1
2
3
4
5
|
func dispatch_source_create(
type: dispatch_source_type_t,
handle: UInt,
mask: UInt,
queue: dispatch_queue_t!) -> dispatch_source_t!
|
作为第一个参数,type: dispatch_source_type_t决定了接下来的句炳以及掩码参数的类型。你可以去看一下相关内容的官方文档以便理解每一种dispatch_source_type_t的用法与解释。
在这里你将监管DISPATCH_SOURCE_TYPE_SIGNAL。如官方文档里解释的那样:派发源监管着当前进程的信号,其句炳的值是一个整数(int),而掩码值暂时没有用到而被设为0。
这些Unix信号可以在名为signal.h的头文件中找到。文件的顶端有很多的#defines。在这堆信号中你将对SIGSTOP信号进行监管。当进程收到一个不可避免的暂停挂起指令时这个信号将被发送。当你用LLDB的debugger除错的时候同样的信号也会被发送。
打开PhotoCollectionViewController.swift文件,在viewDidLoad函数附近添加如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#if DEBUG
private
var
signalSource: dispatch_source_t!
private
var
signalOnceToken = dispatch_once_t()
#endif
override func viewDidLoad() {
super
.viewDidLoad()
#if DEBUG // 1
dispatch_once(&signalOnceToken) {
// 2
let queue = dispatch_get_main_queue()
self.signalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL,
UInt(SIGSTOP), 0, queue)
// 3
if
let source = self.signalSource {
// 4
dispatch_source_set_event_handler(source) {
// 5
NSLog(
"Hi, I am: \(self.description)"
)
}
dispatch_resume(source)
// 6
}
}
#endif
// The other stuff
}
|
分布解释如下:
-
从安全角度考虑,你最好在DEBUG模式下编译代码,以防其他人间接地看到你的代码。: ] 通过在以下路径,Project Settings -> Build Settings -> Swift Compiler – Custom Flags -> Other Swift Flags -> Debug,通过在Debug选项中添加-D DEBUG的方式来定义DEBUG。
-
利用dispatch_once对派发源进行单次的初始化设定。
-
在这里你实例化了一个signalSource变量并表明你只想进行信号监管并将SIGSTOP信号作为第二个参数。再有一点你需要知道的是你使用主队列处理接收到的事件--至于为什么,你待会儿就会知道了。
-
假如你提供了一个错误的参数,派发源对象将不会被创建成功。总之,在使用派发源对象前你要确定你已经创建了一个可用的派发源对象。
-
dispatch_source_set_event_handler注册了一个在收到特定任务信号时将被调用的事件处理闭包。
-
在默认的情况下,所有派发源都是在暂停挂起的状态下开始执行的。当你打算开始监管事件时,你必须让派发源对象从新开始运行。
编译并运行App;以debugger方式暂停App的运行并再立刻恢复运行。检查一下控制台,你会看到如下的反馈:
1
|
2014-08-12 12:24:00.514 GooglyPuff[24985:5481978] Hi, I am:
|
在某种程度上你的App现在知道debug了。这非常不错,但是如何较为实际地使用它呢?
当你从新恢复App运行时你可以使用它对一个object进行debug并显示其相关数据;当某些不怀好意的人想利用debugger影响App正常运行的时候,你也可以为你的App写一个自定义安全登录模块。
另一个有趣的想法便是通过上述机制实现一个对于debugger中对象的堆栈跟踪器。
What ?
考虑一下这种情况,你意外的停止了debugger的运行并在很大程度上debugger很难待在预计的栈帧上。但现在你可以任何时间停止debugger的运行并任何地方执行代码。假如你想在App的特定位置中执行代码,上述情况将会非常有用。
在viewDidLoad的事件处理闭包中的NSLog代码旁添加断点。在debugger中暂停运行,在恢复运行;你的App将运行至断点添加处。现在你便可以随心所欲地访问PhotoCollectionViewController中的实例了。
假如你不知道debugger中有哪些线程,可以去查看一下。主线程总会是第一个被libdispatch跟随的线程;GCD的协调器总会是第二个线程;其他的线程就要视具体情况而定了。
利用断点功能你可以逐步地更新UI、测试类的属性甚至在不重新运行App的情况下执行特定的方法,看起来很方便吧!
Where to Go From Here?
你可以在这里下载到教程的完整代码。
我挺不喜欢唠叨的,但是我觉得你还是应该去看看这篇关于如何使用Xcode中的Instruments的教程。假如你打算对你的App进行优化的话,你是绝对会用到Instruments的。要知道Instruments对于推断相对执行的问题是很有用处的:对比不同代码块中哪一块的相对执行时间是最长的。
与此同时,你也有必要去看看这篇How to Use NSOperations and NSOperationsQueue Tutorial in Swift的教程。NSOperations可以提供更良好的控制,实现多并发任务最大数量的处理以及在牺牲一定运行速度的情况下使得程序更加面向对象化。
记住!在大多数情况下,除非你有着特殊的原因,一定要尽量使用更高级别的API。只有当你真的想到学到或做到一些非常有趣的事情时再去探索苹果的Dark Arts吧!
祝你好运!