Grand Central Dispatch Tutorial for Swift: Part 2/2
2015-06-10 14:11
615 查看
原文地址:http://www.raywenderlich.com/79150/grand-central-dispatch-tutorial-swift-part-2
If you're new here, you may want to subscribe to my RSS
feed or follow me on Twitter.
Thanks for visiting!
Learn about concurrency in this Grand Central Dispatch in-depth tutorial series.
Update April 10, 2015: Updated for Xcode 6.3 / Swift 1.2
Update note: This tutorial was updated for iOS 8 and Swift by Bjørn
Ruud. Original
post by Tutorial Team member Derek
Selander.
Welcome to the second and final part of this Grand Central Dispatch tutorial series!
In the first part of this
series, you learned about concurrency, threading, and how GCD works. You made the
thread safe for reading and writing of photos using a combination of
In addition to all that, you enhanced the UX of the app through the timing of a prompt with
and offloaded the work from the instantiation of a view controller to perform a CPU intensive task with
If you have been following along, you can pick up where you left off with the sample project form Part 1. If you haven’t completed Part 1 or don’t want to reuse your project you candownload
the finished project from the first part of this tutorial here.
It’s time to explore some more GCD!
You may have noticed that when you try to add photos with the Le Internet option, an
alert view pops up well before the images have finished downloading, as shown in the screenshot below:
The fault lies with
has been reproduced below:
Here you call the
of the method — you’re assuming all of the photo downloads have completed. But unfortunately, there’s no guarantee that all the downloads have finished by this point.
The
starts downloading a file from a URL and returns immediatelybefore the download completes. In other words,
its own completion closure at the end, as if its own method body were all straight-line synchronous code and every method call that completed had finished its work.
However,
returns immediately — so this approach won’t work.
Instead,
its own completion closure only after all the image download tasks have called their own completion closures. The question is: how do you monitor concurrent asynchronous events? You don’t know when they will complete, and they can finish in any order.
Perhaps you could write some hacky code that uses multiple
to keep track of each download, but that doesn’t scale well, and frankly, it makes for pretty ugly code.
Fortunately, this type of multiple asynchronous completion monitoring is exactly what dispatch groups were
designed for.
Dispatch groups notify you when an entire group of tasks completes. These tasks can be either asynchronous or synchronous and can even be tracked from different queues. Dispatch groups also notify you in synchronous or asynchronous fashion when all of the group’s
events are complete. Since items are being tracked on different queues, an instance of
track of the different tasks in the queues.
The GCD API provides two ways to be notified when all events in the group have completed.
The first one,
that blocks your current thread and waits until either all the tasks in the group have completed, or until a timeout occurs. This is exactly what you want in this case.
Open PhotoManager.swift and replace
the following implementation:
Taking each numbered comment in turn, you’ll see the following:
Since you’re using the synchronous
blocks the current thread, you use
place the entire method into a background queue to ensure you don’t block the main thread.
This creates a new dispatch group which behaves somewhat like a counter of the number of uncompleted tasks.
that a task has started. You must balance out the number of
with the number of
or else your app will crash.
Here you manually notify the group that this work is done. Again, you’re balancing all group enters with an equal amount of group leaves.
the tasks are complete or until the time expires. If the time expires before all events complete, the function will return a non-zero result. You could put this into a conditional closure to check if the waiting period expired; however, in this case you specified
for it to wait forever by supplying
This means, unsurprisingly, it’ll wait forever! That’s fine, because the completion of the photos creation will always complete.
At this point, you are guaranteed that all image tasks have either completed or timed out. You then make a call back to the main queue to run your completion closure. This will append work onto the main thread to be executed at some later time.
Finally, run the completion closure if one was supplied.
Build and run your app, attempt to download multiple images and notice how your app behaves with the completion closure in place.
Note: If the network activities occur too quickly to discern when the completion closure should be called
and you’re running the app on a device, you can make sure this really works by toggling some network settings in the Developer
Section of the Settings app. Just go to the Network
Link Conditioner section, enable it, and select a profile. “Very Bad Network” is a good choice.
If you are running on the Simulator, you can use the Network
Link Conditioner included in the Hardware IO Tools for Xcode to change your network speed. This is a good tool to have in your arsenal because it forces you to be conscious of what happens to your apps when connection speeds are less than optimal.
This solution is good so far, but in general it’s best to avoid blocking threads if at all possible. Your next task is to rewrite the same method to notify you asynchronously when all the downloads have completed.
Before we head on to another use of dispatch groups, here’s a brief guide on when and how to use dispatch groups with the various queue types:
Custom Serial Queue: This is a good candidate for notifications when a group of tasks
completes.
Main Queue (Serial): This is a good candidate as well in this scenario. You should
be wary of using this on the main queue if you are waiting synchronously for the completion of all work since you don’t want to hold up the main thread. However, the asynchronous model is an attractive way to update the UI once several long-running tasks finish,
such as network calls.
Concurrent Queue: This as well is a good candidate for dispatch groups and completion
notifications.
That’s all well and good, but it’s a bit clumsy to have to dispatch asynchronously onto another queue and then block using dispatch_group_wait.
There’s another way…
Find
replace it with this implementation:
Here’s how your new asynchronous method works:
In this new implementation you don’t need to surround the method in a
since you’re not blocking the main thread.
completion closure. This code executes when there are no more items left in the dispatch group and it’s the completion closure’s turn to run. You also specify on which queue to run your completion code. Here, the main queue is the one you want.
This is a much cleaner way to handle this particular job and doesn’t block any threads.
With all of these new tools at your disposal, you should probably thread everything, right!?
Take a look at
You might notice that there’s a
that cycles through three iterations and downloads three separate images. Your job is to see if you can run this
concurrently to try and speed it up.
This is a job for
which executes different iterations concurrently. This function is sychronous, so just like a normal
only when all of the work is done.
Care must be taken when figuring out the optimal amount of iterations for any given amount of work inside the closure, since many iterations and a small amount of work per iteration can create so much overhead that it negates any gains from making the calls
concurrent. The technique known as striding helps you out here. This is where for
each iteration you do multiple pieces of work.
When is it appropriate to use
Custom Serial Queue: A serial queue would completely negate the use of
you might as well just use a normal
Main Queue (Serial): Just as above, using this on a serial queue is a bad idea. Just
use a normal
Concurrent Queue: This is a good choice for concurrent looping, especially if you need
to track the progress of your tasks.
Head back to
replace it with the following implementation:
Your loop is now running concurrently; in the code above, in the call to dispatch_apply,
you supply the amount of iterations with the first parameter, the queue to perform the tasks on in the second parameter and the closure action in the third parameter.
Be aware that although you have code that will add the photos in a thread safe manner, the ordering of the images could be different depending on which thread finishes first.
Build and run, then add some photos from Le Internet. Notice anything different?
Running this new code on the device will occasionally produce marginally faster results. But was all this work worth it?
Actually, it’s not worth it in this case. Here’s why:
You’ve probably created more overhead running the threads in parallel than just running the
in the first place. You should use
iterating over very large sets along with the appropriate stride length.
Your time to create an app is limited — don’t waste time pre-optimizing code that you don’t know is broken. If you’re going to optimize something, optimize something that is noticeable and worth your time. Find the methods with the longest execution times by
profiling your app in Instruments. Check out How
to Use Instruments in Xcode to learn more.
Typically, optimizing code makes your code more complicated for yourself and for other developers coming after you. Make sure the added complication is worth the benefit.
Remember, don’t go crazy with optimizations. You’ll only make it harder on yourself and others who have to wade through your code.
New in iOS 8 and OS X Yosemite is the introduction of dispatch block objects. These
are implemented as a wrapper around ordinary closures and behave just like them. Dispatch block objects can do a number of things, like set a Quality of Service class per object for internal prioritization in a queue, but most notably is the ability to cancel
the execution of block objects. Be aware that a block object can only be cancelled before it reaches the head of a queue and start executing.
Let’s demonstrate this by starting download tasks for several copies of images from Le Internet,
and then cancelling some of them. Select PhotoManager.swift and replace
the following:
The
of each address.
This array will hold the created block objects for later use.
The first parameter is a flag defining various block traits. The flag used here makes the block inherit its QoS class from the queue it is dispatched to. The second parameter is the block definition in the form of a closure.
Here the block is dispatched asynchronously to the global main queue. For this example using the main queue makes it easier to cancel select blocks since it’s a serial queue. The code that sets up the dispatch blocks is already executing on the main queue so
you are guaranteed that the download blocks will execute at some later time.
The first three downloads are left alone, and the array is sliced to get the rest.
0 and an upper bound (not inclusive). By using 2 for the upper bound you get either a 0 or a 1, like a coin toss.
If the random number is 1 the block is cancelled. That is, if the block is still in a queue and has not begun executing. Blocks can not be cancelled in the middle of execution.
Since all blocks are added to the dispatch group, remember to remove the cancelled ones.
Build and run, and add images from Le Internet. You’ll see that the app now downloads
three of each image and a random extra amount. The rest have been cancelled after being dispatched to a queue. This is a pretty contrived example but it illustrates how dispatch block objects are used and cancelled nicely.
Dispatch block objects can do a lot more, so be sure to check out the documentation.
But wait! There’s more! Here are some extra functions that are a little farther off the beaten path. Although you won’t use these tools nearly as frequently, they can be tremendously helpful in the right situations.
This might sound like a crazy idea, but did you know that Xcode has testing functionality? :] I know, sometimes I like to pretend it’s not there, but writing and running tests is important when building complex relationships in code.
Testing in Xcode is performed on subclasses of
runs any method in its method signature that begins with
Testing is measured on the main thread, so you can assume that every test happens in a serial manner.
As soon as a given test method completes, XCTest methods will consider a test to be
finished and move onto the next test. That means that any asynchronous code from the previous test will continue to run while the next test is running.
Networking code is usually asynchronous, since you don’t want to block the main thread while performing a network fetch. That, coupled with the fact that tests finish when the test method finishes, can make it hard to test networking code.
Let’s take a brief look at two common techniques for testing asynchronous code: one using semaphores and
one using expectations.
Semaphores are an old-school threading concept introduced to the world by the ever-so-humble Edsger W. Dijkstra. Semaphores are a complex topic because they build upon the intricacies of operating system functions.
If you want to learn more about semaphores, check out this link
which discusses semaphore theory in more detail. If you’re the academic type, a classic software development problem that uses semaphores is the Dining
Philosophers Problem.
Semaphores lets you control the access of multiple consumers into a finite amount of resources. For example, if you created a semaphore with a pool of two resources, at most only two threads could access the critical section at the same time. Other items that
want to use the resource must wait in a FIFO queue.
Open GooglyPuffTests.swift and replace downloadImageURLWithString with
the following implementation:
Here’s how the semaphore works in the code above:
Create the semaphore. The parameter indicates the value the semaphore starts with. This number is the number of things that can access the semaphore without having to have something increment it first (note that incrementing a semaphore is known as signalling
it).
In the completion closure you tell the semaphore that you no longer need the resource. This increments the semaphore count and signals that the semaphore is available to other resources that want it.
This waits on the semaphore, with a given timeout. This call blocks the current thread until the semaphore has been signalled. A non-zero return code from this function means that the timeout was reached. In this case, the test is failed because it is deemed
that the network should not take more than 10 seconds to return — a fair point!
Run your tests by selecting Product / Test from the menu or use ⌘+U if
you have the default key bindings. They should all succeed in a timely manner.
Disable your connection and run the tests again; if you are running on a device, put it in airplane mode. If you’re running on the simulator then simply turn off your connection. The tests complete with a fail result, after 10 seconds. Great, it worked!
These are rather trivial tests, but if you are working with a server team then these basic tests can prevent a wholesome round of finger-pointing of who is to blame for the latest network issue.
The
to the asynchronous code testing problem in the form ofexpectations. This feature
lets you set up an expectation – something you expect will happen – and then start an asynchronous task. Then you can have the test runner wait until the asynchronous task marks the expectation as fulfilled.
Navigate to GooglyPuffTests.swift and replace
the following code:
Here is how it works:
Create the expectation with
The test runner will display the string parameter here in the test log upon failure, so describe what you expect to happen.
Call
to mark it as fulfilled.
The calling thread waits for expectations to be fulfilled by calling
If the wait times out, that’s treated as an error.
Build and run the tests. The end result is not very different from using a semaphore, but leveraging the
is a cleaner and more readable solution.
A particularly interesting feature of GCD is Dispatch Sources, which are basically
a grab-bag of low-level functionality helping you to respond to or monitor Unix signals, file descriptors, Mach ports, VFS Nodes, and other obscure stuff. All of this is far beyond the scope of this tutorial, but you’ll get a small taste of it by implementing
a dispatch source object and using it in a rather peculiar way.
First-time users of dispatch sources can get quite lost on how to use a source, so the first thing you need to understand how
This is the function prototype for creating a source:
The first parameter,
is the most important parameter as it dictates what the handle and mask parameters will be. You’ll need to refer to the Xcode
documentation to see what options are available for each
Here you’ll be monitoring for
As the documentation
says:
A dispatch source that monitors the current process for signals. The handle is a signal number (int). The mask is unused (pass zero for now).
A list of these Unix signals can found in the header file
At the top there are a bunch of
that list of signals, you will be monitoring the
This signal is sent when a process receives an unavoidable suspend instruction. This is the same signal that’s sent when you debug your application using the LLDB debugger.
Go to PhotoCollectionViewController.swift and add the following code near
You’ll want to add these two private properties to the class, and this new block of code at the beginning of
after the call to the superclass but before the existing line that gets the
The code is a little involved, so step through the code one comment at a time to see what’s going on:
It’s best to only compile this code while in DEBUG mode since this could give “interested parties” a lot of insight into your app. :] DEBUG is defined by adding -D
DEBUG under Project Settings -> Build Settings -> Swift Compiler – Custom Flags ->
Other Swift Flags -> Debug.
Use
one-time setup.
Here you instantiate the
You indicate that you’re interested in signal monitoring and provide the
as the second parameter. Additionally, you use the main queue for handling received events — you’ll discover why shortly.
A dispatch source object won’t be created if you provide malformed parameters. As a result, you should make sure you have a valid dispatch source object before working on it.
event handler closure that is invoked when you receive the signal you’re monitoring for.
By default, all sources start in the suspended state. You must tell the source object to resume when you want to start monitoring for the events.
Build and run your app; pause in the debugger and resume the app immediately. Check out the console, and you’ll see something like this in the debugger:
You app is now debugging-aware! That’s pretty awesome, but how would you use this in real life?
You could use this to debug an object and display data whenever you resume the app; you could also give your app custom security logic to protect itself (or the user’s data) when malicious attackers attach a debugger to your application.
An interesting idea is to use this approach as a stack trace tool to find the object you want to manipulate in the debugger.
Think about that situation for a second. When you stop the debugger out of the blue, you’re almost never on the desired stack frame. Now you can stop the debugger at anytime and have code execute at your desired location. This is very useful if you want to
execute code at a point in your app that’s tedious to access from the debugger. Try it out!
Put a breakpoint on the
the event handler you just added. Pause in the debugger, then start again; the app will then hit the breakpoint you added. You’re now deep in the depths of your
Now you can access the instance of
your heart’s content. Pretty handy!
Note: If you haven’t already noticed which threads are which in the debugger, take a look at them now. The
main thread will always be the first thread followed by libdispatch, the coordinator for GCD, as the second thread. After that, the thread count and remaining threads depend on what the hardware was doing when the app hit the breakpoint.
In the debugger console, type the following:
The Xcode 6.3 debugger can sometimes be uncooperative. If you get the message
then you have to do it the hard way to work around a bug in LLDB. First take note of the address of
the debug area, then manually cast the value to the type you want:
Then resume execution of the app. You’ll see the following:
With this method, you can make updates to the UI, inquire about the properties of a class, and even execute methods — all while not having to restart the app to get into that special workflow state. Pretty neat.
You can download
the final project here.
I hate to bang on this subject again, but you really should check out the How
to Use Instruments tutorial. You’ll definitely need this if you plan on doing any optimization of your apps. Be aware that Instruments is good for profiling relative execution: comparing which areas of code takes longer in relation to other areas. If you’re
trying to figure out the actual execution time of a method, you might need to come up with a more home-brewed solution.
Also check out How
to Use NSOperations and NSOperationQueue Tutorial in Swift, a concurrency technology that is built on top of GCD. In general, it’s best practice to use GCD if you are using simple fire-and-forget tasks. NSOperations offers better control, an implementation
for handling maximum concurrent operations, and a more object-oriented paradigm at the cost of speed.
Remember, unless you have a specific reason to go lower, always try and stick with a higher level API. Only venture into the dark arts of Apple if you want to learn more or to do something really, really “interesting”. :]
Good luck and have fun! Post any questions or feedback in the discussion below!
If you're new here, you may want to subscribe to my RSS
feed or follow me on Twitter.
Thanks for visiting!
Learn about concurrency in this Grand Central Dispatch in-depth tutorial series.
Update April 10, 2015: Updated for Xcode 6.3 / Swift 1.2
Update note: This tutorial was updated for iOS 8 and Swift by Bjørn
Ruud. Original
post by Tutorial Team member Derek
Selander.
Welcome to the second and final part of this Grand Central Dispatch tutorial series!
In the first part of this
series, you learned about concurrency, threading, and how GCD works. You made the
PhotoManagersingleton
thread safe for reading and writing of photos using a combination of
dispatch_barrier_asyncand
dispatch_sync.
In addition to all that, you enhanced the UX of the app through the timing of a prompt with
dispatch_after,
and offloaded the work from the instantiation of a view controller to perform a CPU intensive task with
dispatch_async.
If you have been following along, you can pick up where you left off with the sample project form Part 1. If you haven’t completed Part 1 or don’t want to reuse your project you candownload
the finished project from the first part of this tutorial here.
It’s time to explore some more GCD!
Correcting the Premature Popup
You may have noticed that when you try to add photos with the Le Internet option, analert view pops up well before the images have finished downloading, as shown in the screenshot below:
The fault lies with
PhotoManager’s
downloadPhotosWithCompletionwhich
has been reproduced below:
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) } } |
completionclosure at the end
of the method — you’re assuming all of the photo downloads have completed. But unfortunately, there’s no guarantee that all the downloads have finished by this point.
The
DownloadPhotoclass’s instantiation method
starts downloading a file from a URL and returns immediatelybefore the download completes. In other words,
downloadPhotosWithCompletioncalls
its own completion closure at the end, as if its own method body were all straight-line synchronous code and every method call that completed had finished its work.
However,
DownloadPhoto(url:)is asynchronous and
returns immediately — so this approach won’t work.
Instead,
downloadPhotosWithCompletionshould call
its own completion closure only after all the image download tasks have called their own completion closures. The question is: how do you monitor concurrent asynchronous events? You don’t know when they will complete, and they can finish in any order.
Perhaps you could write some hacky code that uses multiple
Boolvalues
to keep track of each download, but that doesn’t scale well, and frankly, it makes for pretty ugly code.
Fortunately, this type of multiple asynchronous completion monitoring is exactly what dispatch groups were
designed for.
Dispatch Groups
Dispatch groups notify you when an entire group of tasks completes. These tasks can be either asynchronous or synchronous and can even be tracked from different queues. Dispatch groups also notify you in synchronous or asynchronous fashion when all of the group’sevents are complete. Since items are being tracked on different queues, an instance of
dispatch_group_tkeeps
track of the different tasks in the queues.
The GCD API provides two ways to be notified when all events in the group have completed.
The first one,
dispatch_group_wait, is a function
that blocks your current thread and waits until either all the tasks in the group have completed, or until a timeout occurs. This is exactly what you want in this case.
Open PhotoManager.swift and replace
downloadPhotosWithCompletionwith
the following implementation:
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) } } } } |
Since you’re using the synchronous
dispatch_group_waitwhich
blocks the current thread, you use
dispatch_asyncto
place the entire method into a background queue to ensure you don’t block the main thread.
This creates a new dispatch group which behaves somewhat like a counter of the number of uncompleted tasks.
dispatch_group_entermanually notifies a group
that a task has started. You must balance out the number of
dispatch_group_entercalls
with the number of
dispatch_group_leavecalls
or else your app will crash.
Here you manually notify the group that this work is done. Again, you’re balancing all group enters with an equal amount of group leaves.
dispatch_group_waitwaits until either all of
the tasks are complete or until the time expires. If the time expires before all events complete, the function will return a non-zero result. You could put this into a conditional closure to check if the waiting period expired; however, in this case you specified
for it to wait forever by supplying
DISPATCH_TIME_FOREVER.
This means, unsurprisingly, it’ll wait forever! That’s fine, because the completion of the photos creation will always complete.
At this point, you are guaranteed that all image tasks have either completed or timed out. You then make a call back to the main queue to run your completion closure. This will append work onto the main thread to be executed at some later time.
Finally, run the completion closure if one was supplied.
Build and run your app, attempt to download multiple images and notice how your app behaves with the completion closure in place.
Note: If the network activities occur too quickly to discern when the completion closure should be called
and you’re running the app on a device, you can make sure this really works by toggling some network settings in the Developer
Section of the Settings app. Just go to the Network
Link Conditioner section, enable it, and select a profile. “Very Bad Network” is a good choice.
If you are running on the Simulator, you can use the Network
Link Conditioner included in the Hardware IO Tools for Xcode to change your network speed. This is a good tool to have in your arsenal because it forces you to be conscious of what happens to your apps when connection speeds are less than optimal.
This solution is good so far, but in general it’s best to avoid blocking threads if at all possible. Your next task is to rewrite the same method to notify you asynchronously when all the downloads have completed.
Before we head on to another use of dispatch groups, here’s a brief guide on when and how to use dispatch groups with the various queue types:
Custom Serial Queue: This is a good candidate for notifications when a group of tasks
completes.
Main Queue (Serial): This is a good candidate as well in this scenario. You should
be wary of using this on the main queue if you are waiting synchronously for the completion of all work since you don’t want to hold up the main thread. However, the asynchronous model is an attractive way to update the UI once several long-running tasks finish,
such as network calls.
Concurrent Queue: This as well is a good candidate for dispatch groups and completion
notifications.
Dispatch Groups, Take Two
That’s all well and good, but it’s a bit clumsy to have to dispatch asynchronously onto another queue and then block using dispatch_group_wait.There’s another way…
Find
downloadPhotosWithCompletionin PhotoManager.swift and
replace it with this implementation:
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) } } } |
In this new implementation you don’t need to surround the method in a
dispatch_asynccall
since you’re not blocking the main thread.
dispatch_group_notifyserves as the asynchronous
completion closure. This code executes when there are no more items left in the dispatch group and it’s the completion closure’s turn to run. You also specify on which queue to run your completion code. Here, the main queue is the one you want.
This is a much cleaner way to handle this particular job and doesn’t block any threads.
The Perils of Too Much Concurrency
With all of these new tools at your disposal, you should probably thread everything, right!?Take a look at
downloadPhotosWithCompletionin
PhotoManager.
You might notice that there’s a
forloop in there
that cycles through three iterations and downloads three separate images. Your job is to see if you can run this
forloop
concurrently to try and speed it up.
This is a job for
dispatch_apply.
dispatch_applyacts like a
forloop
which executes different iterations concurrently. This function is sychronous, so just like a normal
forloop,
dispatch_applyreturns
only when all of the work is done.
Care must be taken when figuring out the optimal amount of iterations for any given amount of work inside the closure, since many iterations and a small amount of work per iteration can create so much overhead that it negates any gains from making the calls
concurrent. The technique known as striding helps you out here. This is where for
each iteration you do multiple pieces of work.
When is it appropriate to use
dispatch_apply?
Custom Serial Queue: A serial queue would completely negate the use of
dispatch_apply;
you might as well just use a normal
forloop.
Main Queue (Serial): Just as above, using this on a serial queue is a bad idea. Just
use a normal
forloop.
Concurrent Queue: This is a good choice for concurrent looping, especially if you need
to track the progress of your tasks.
Head back to
downloadPhotosWithCompletionand
replace it with the following implementation:
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) } } } |
you supply the amount of iterations with the first parameter, the queue to perform the tasks on in the second parameter and the closure action in the third parameter.
Be aware that although you have code that will add the photos in a thread safe manner, the ordering of the images could be different depending on which thread finishes first.
Build and run, then add some photos from Le Internet. Notice anything different?
Running this new code on the device will occasionally produce marginally faster results. But was all this work worth it?
Actually, it’s not worth it in this case. Here’s why:
You’ve probably created more overhead running the threads in parallel than just running the
forloop
in the first place. You should use
dispatch_applyfor
iterating over very large sets along with the appropriate stride length.
Your time to create an app is limited — don’t waste time pre-optimizing code that you don’t know is broken. If you’re going to optimize something, optimize something that is noticeable and worth your time. Find the methods with the longest execution times by
profiling your app in Instruments. Check out How
to Use Instruments in Xcode to learn more.
Typically, optimizing code makes your code more complicated for yourself and for other developers coming after you. Make sure the added complication is worth the benefit.
Remember, don’t go crazy with optimizations. You’ll only make it harder on yourself and others who have to wade through your code.
Cancelling Dispatch Blocks
New in iOS 8 and OS X Yosemite is the introduction of dispatch block objects. Theseare implemented as a wrapper around ordinary closures and behave just like them. Dispatch block objects can do a number of things, like set a Quality of Service class per object for internal prioritization in a queue, but most notably is the ability to cancel
the execution of block objects. Be aware that a block object can only be cancelled before it reaches the head of a queue and start executing.
Let’s demonstrate this by starting download tasks for several copies of images from Le Internet,
and then cancelling some of them. Select PhotoManager.swift and replace
downloadPhotosWithCompletionwith
the following:
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) } } } |
addressesarray is expanded to hold three
of each address.
This array will hold the created block objects for later use.
dispatch_block_createcreates a new block object.
The first parameter is a flag defining various block traits. The flag used here makes the block inherit its QoS class from the queue it is dispatched to. The second parameter is the block definition in the form of a closure.
Here the block is dispatched asynchronously to the global main queue. For this example using the main queue makes it easier to cancel select blocks since it’s a serial queue. The code that sets up the dispatch blocks is already executing on the main queue so
you are guaranteed that the download blocks will execute at some later time.
The first three downloads are left alone, and the array is sliced to get the rest.
arc4random_uniformprovides an integer between
0 and an upper bound (not inclusive). By using 2 for the upper bound you get either a 0 or a 1, like a coin toss.
If the random number is 1 the block is cancelled. That is, if the block is still in a queue and has not begun executing. Blocks can not be cancelled in the middle of execution.
Since all blocks are added to the dispatch group, remember to remove the cancelled ones.
Build and run, and add images from Le Internet. You’ll see that the app now downloads
three of each image and a random extra amount. The rest have been cancelled after being dispatched to a queue. This is a pretty contrived example but it illustrates how dispatch block objects are used and cancelled nicely.
Dispatch block objects can do a lot more, so be sure to check out the documentation.
Miscellaneous GCD Fun
But wait! There’s more! Here are some extra functions that are a little farther off the beaten path. Although you won’t use these tools nearly as frequently, they can be tremendously helpful in the right situations.
Testing Asynchronous Code
This might sound like a crazy idea, but did you know that Xcode has testing functionality? :] I know, sometimes I like to pretend it’s not there, but writing and running tests is important when building complex relationships in code.Testing in Xcode is performed on subclasses of
XCTestCaseand
runs any method in its method signature that begins with
test.
Testing is measured on the main thread, so you can assume that every test happens in a serial manner.
As soon as a given test method completes, XCTest methods will consider a test to be
finished and move onto the next test. That means that any asynchronous code from the previous test will continue to run while the next test is running.
Networking code is usually asynchronous, since you don’t want to block the main thread while performing a network fetch. That, coupled with the fact that tests finish when the test method finishes, can make it hard to test networking code.
Let’s take a brief look at two common techniques for testing asynchronous code: one using semaphores and
one using expectations.
Semaphores
Semaphores are an old-school threading concept introduced to the world by the ever-so-humble Edsger W. Dijkstra. Semaphores are a complex topic because they build upon the intricacies of operating system functions.If you want to learn more about semaphores, check out this link
which discusses semaphore theory in more detail. If you’re the academic type, a classic software development problem that uses semaphores is the Dining
Philosophers Problem.
Semaphores lets you control the access of multiple consumers into a finite amount of resources. For example, if you created a semaphore with a pool of two resources, at most only two threads could access the critical section at the same time. Other items that
want to use the resource must wait in a FIFO queue.
Open GooglyPuffTests.swift and replace downloadImageURLWithString with
the following implementation:
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") } } |
Create the semaphore. The parameter indicates the value the semaphore starts with. This number is the number of things that can access the semaphore without having to have something increment it first (note that incrementing a semaphore is known as signalling
it).
In the completion closure you tell the semaphore that you no longer need the resource. This increments the semaphore count and signals that the semaphore is available to other resources that want it.
This waits on the semaphore, with a given timeout. This call blocks the current thread until the semaphore has been signalled. A non-zero return code from this function means that the timeout was reached. In this case, the test is failed because it is deemed
that the network should not take more than 10 seconds to return — a fair point!
Run your tests by selecting Product / Test from the menu or use ⌘+U if
you have the default key bindings. They should all succeed in a timely manner.
Disable your connection and run the tests again; if you are running on a device, put it in airplane mode. If you’re running on the simulator then simply turn off your connection. The tests complete with a fail result, after 10 seconds. Great, it worked!
These are rather trivial tests, but if you are working with a server team then these basic tests can prevent a wholesome round of finger-pointing of who is to blame for the latest network issue.
Expectations
The XCTestframework provides another solution
to the asynchronous code testing problem in the form ofexpectations. This feature
lets you set up an expectation – something you expect will happen – and then start an asynchronous task. Then you can have the test runner wait until the asynchronous task marks the expectation as fulfilled.
Navigate to GooglyPuffTests.swift and replace
downloadImageURLWithStringwith
the following code:
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) } } } |
Create the expectation with
expectationWithDescription.
The test runner will display the string parameter here in the test log upon failure, so describe what you expect to happen.
Call
fulfillin the closure that executes asynchronously
to mark it as fulfilled.
The calling thread waits for expectations to be fulfilled by calling
waitForExpectationsWithTimeout.
If the wait times out, that’s treated as an error.
Build and run the tests. The end result is not very different from using a semaphore, but leveraging the
XCTestframework
is a cleaner and more readable solution.
Working With Dispatch Sources
A particularly interesting feature of GCD is Dispatch Sources, which are basicallya grab-bag of low-level functionality helping you to respond to or monitor Unix signals, file descriptors, Mach ports, VFS Nodes, and other obscure stuff. All of this is far beyond the scope of this tutorial, but you’ll get a small taste of it by implementing
a dispatch source object and using it in a rather peculiar way.
First-time users of dispatch sources can get quite lost on how to use a source, so the first thing you need to understand how
dispatch_source_createworks.
This is the function prototype for creating a source:
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,
is the most important parameter as it dictates what the handle and mask parameters will be. You’ll need to refer to the Xcode
documentation to see what options are available for each
dispatch_source_type_tparameter.
Here you’ll be monitoring for
DISPATCH_SOURCE_TYPE_SIGNAL.
As the documentation
says:
A dispatch source that monitors the current process for signals. The handle is a signal number (int). The mask is unused (pass zero for now).
A list of these Unix signals can found in the header file
signal.h.
At the top there are a bunch of
#defines. From
that list of signals, you will be monitoring the
SIGSTOPsignal.
This signal is sent when a process receives an unavoidable suspend instruction. This is the same signal that’s sent when you debug your application using the LLDB debugger.
Go to PhotoCollectionViewController.swift and add the following code near
viewDidLoad.
You’ll want to add these two private properties to the class, and this new block of code at the beginning of
viewDidLoad,
after the call to the superclass but before the existing line that gets the
ALAssetLibrary:
#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 } |
It’s best to only compile this code while in DEBUG mode since this could give “interested parties” a lot of insight into your app. :] DEBUG is defined by adding -D
DEBUG under Project Settings -> Build Settings -> Swift Compiler – Custom Flags ->
Other Swift Flags -> Debug.
Use
dispatch_onceto perform the dispatch source’s
one-time setup.
Here you instantiate the
signalSourcevariable.
You indicate that you’re interested in signal monitoring and provide the
SIGSTOPsignal
as the second parameter. Additionally, you use the main queue for handling received events — you’ll discover why shortly.
A dispatch source object won’t be created if you provide malformed parameters. As a result, you should make sure you have a valid dispatch source object before working on it.
dispatch_source_set_event_handlerregisters an
event handler closure that is invoked when you receive the signal you’re monitoring for.
By default, all sources start in the suspended state. You must tell the source object to resume when you want to start monitoring for the events.
Build and run your app; pause in the debugger and resume the app immediately. Check out the console, and you’ll see something like this in the debugger:
2014-08-12 12:24:00.514 GooglyPuff[24985:5481978] Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7b765ee0> |
You could use this to debug an object and display data whenever you resume the app; you could also give your app custom security logic to protect itself (or the user’s data) when malicious attackers attach a debugger to your application.
An interesting idea is to use this approach as a stack trace tool to find the object you want to manipulate in the debugger.
Think about that situation for a second. When you stop the debugger out of the blue, you’re almost never on the desired stack frame. Now you can stop the debugger at anytime and have code execute at your desired location. This is very useful if you want to
execute code at a point in your app that’s tedious to access from the debugger. Try it out!
Put a breakpoint on the
NSLogstatement in
viewDidLoadin
the event handler you just added. Pause in the debugger, then start again; the app will then hit the breakpoint you added. You’re now deep in the depths of your
PhotoCollectionViewControllermethod.
Now you can access the instance of
PhotoCollectionViewControllerto
your heart’s content. Pretty handy!
Note: If you haven’t already noticed which threads are which in the debugger, take a look at them now. The
main thread will always be the first thread followed by libdispatch, the coordinator for GCD, as the second thread. After that, the thread count and remaining threads depend on what the hardware was doing when the app hit the breakpoint.
In the debugger console, type the following:
(lldb) po self.navigationItem.prompt = "WOOT!" |
error: use of unresolved identifier 'self' |
selfin
the debug area, then manually cast the value to the type you want:
(lldb) e let $vc = unsafeBitCast(0x7fd0b3e22bc0, GooglyPuff.PhotoCollectionViewController.self) (lldb) po $vc.navigationItem.prompt = "WOOT!" |
With this method, you can make updates to the UI, inquire about the properties of a class, and even execute methods — all while not having to restart the app to get into that special workflow state. Pretty neat.
Where to Go From Here?
You can downloadthe final project here.
I hate to bang on this subject again, but you really should check out the How
to Use Instruments tutorial. You’ll definitely need this if you plan on doing any optimization of your apps. Be aware that Instruments is good for profiling relative execution: comparing which areas of code takes longer in relation to other areas. If you’re
trying to figure out the actual execution time of a method, you might need to come up with a more home-brewed solution.
Also check out How
to Use NSOperations and NSOperationQueue Tutorial in Swift, a concurrency technology that is built on top of GCD. In general, it’s best practice to use GCD if you are using simple fire-and-forget tasks. NSOperations offers better control, an implementation
for handling maximum concurrent operations, and a more object-oriented paradigm at the cost of speed.
Remember, unless you have a specific reason to go lower, always try and stick with a higher level API. Only venture into the dark arts of Apple if you want to learn more or to do something really, really “interesting”. :]
Good luck and have fun! Post any questions or feedback in the discussion below!
相关文章推荐
- iOS 下拉刷新和加载更多 (OC\Swift)
- 业界资讯:ActionSwift3
- swift 字符串,数组,字典 的那些事<三>
- swift 字符串,数组,字典 的那些事<二>
- 【swift】15-0610 下标 继承和扩展 初始化和反初始化
- Swift学习笔记十
- swift2.0_语法改动详细说明
- Swift语法之?和!区别
- Swift学习笔记-面向对象3
- 一张图看WWDC 2015
- swift 字符串,数组,字典 的那些事<一>
- 苹果Swift编程语言入门教程【中文版】
- Swift学习笔记九
- 转 Grand Central Dispatch 基础教程:Part 1/2 -swift
- 【Swift】TTTAttributedLabel使用小记
- 总结swift语言常见的20个问题和回答
- swift学习记录(private和public)
- Swift学习笔记八
- Swift 学习笔记一 基本数据类型和简单语句语法
- iOS开发笔记-swift实现iOS数据持久化之归档NSKeyedArchiver